From 6e19484f4d3bf372212f2da462110a1a8c10cbf2 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 29 Jun 2023 18:03:20 +0800 Subject: Sync branches into databases (#22743) Related #14180 Related #25233 Related #22639 Close #19786 Related #12763 This PR will change all the branches retrieve method from reading git data to read database to reduce git read operations. - [x] Sync git branches information into database when push git data - [x] Create a new table `Branch`, merge some columns of `DeletedBranch` into `Branch` table and drop the table `DeletedBranch`. - [x] Read `Branch` table when visit `code` -> `branch` page - [x] Read `Branch` table when list branch names in `code` page dropdown - [x] Read `Branch` table when list git ref compare page - [x] Provide a button in admin page to manually sync all branches. - [x] Sync branches if repository is not empty but database branches are empty when visiting pages with branches list - [x] Use `commit_time desc` as the default FindBranch order by to keep consistent as before and deleted branches will be always at the end. --------- Co-authored-by: Jason Song --- models/git/branch.go | 379 ++++++++++++++++++++++++++++++++++++ models/git/branch_list.go | 132 +++++++++++++ models/git/branch_test.go | 176 +++++++++++++++++ models/git/branches.go | 197 ------------------- models/git/branches_test.go | 162 --------------- models/git/protected_branch_list.go | 37 ++-- 6 files changed, 712 insertions(+), 371 deletions(-) create mode 100644 models/git/branch.go create mode 100644 models/git/branch_list.go create mode 100644 models/git/branch_test.go delete mode 100644 models/git/branches.go delete mode 100644 models/git/branches_test.go (limited to 'models/git') diff --git a/models/git/branch.go b/models/git/branch.go new file mode 100644 index 0000000000..adf8b0a78d --- /dev/null +++ b/models/git/branch.go @@ -0,0 +1,379 @@ +// Copyright 2016 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "context" + "fmt" + "time" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" +) + +// ErrBranchNotExist represents an error that branch with such name does not exist. +type ErrBranchNotExist struct { + RepoID int64 + BranchName string +} + +// IsErrBranchNotExist checks if an error is an ErrBranchDoesNotExist. +func IsErrBranchNotExist(err error) bool { + _, ok := err.(ErrBranchNotExist) + return ok +} + +func (err ErrBranchNotExist) Error() string { + return fmt.Sprintf("branch does not exist [repo_id: %d name: %s]", err.RepoID, err.BranchName) +} + +func (err ErrBranchNotExist) Unwrap() error { + return util.ErrNotExist +} + +// ErrBranchAlreadyExists represents an error that branch with such name already exists. +type ErrBranchAlreadyExists struct { + BranchName string +} + +// IsErrBranchAlreadyExists checks if an error is an ErrBranchAlreadyExists. +func IsErrBranchAlreadyExists(err error) bool { + _, ok := err.(ErrBranchAlreadyExists) + return ok +} + +func (err ErrBranchAlreadyExists) Error() string { + return fmt.Sprintf("branch already exists [name: %s]", err.BranchName) +} + +func (err ErrBranchAlreadyExists) Unwrap() error { + return util.ErrAlreadyExist +} + +// ErrBranchNameConflict represents an error that branch name conflicts with other branch. +type ErrBranchNameConflict struct { + BranchName string +} + +// IsErrBranchNameConflict checks if an error is an ErrBranchNameConflict. +func IsErrBranchNameConflict(err error) bool { + _, ok := err.(ErrBranchNameConflict) + return ok +} + +func (err ErrBranchNameConflict) Error() string { + return fmt.Sprintf("branch conflicts with existing branch [name: %s]", err.BranchName) +} + +func (err ErrBranchNameConflict) Unwrap() error { + return util.ErrAlreadyExist +} + +// ErrBranchesEqual represents an error that base branch is equal to the head branch. +type ErrBranchesEqual struct { + BaseBranchName string + HeadBranchName string +} + +// IsErrBranchesEqual checks if an error is an ErrBranchesEqual. +func IsErrBranchesEqual(err error) bool { + _, ok := err.(ErrBranchesEqual) + return ok +} + +func (err ErrBranchesEqual) Error() string { + return fmt.Sprintf("branches are equal [head: %sm base: %s]", err.HeadBranchName, err.BaseBranchName) +} + +func (err ErrBranchesEqual) Unwrap() error { + return util.ErrInvalidArgument +} + +// Branch represents a branch of a repository +// For those repository who have many branches, stored into database is a good choice +// for pagination, keyword search and filtering +type Branch struct { + ID int64 + RepoID int64 `xorm:"UNIQUE(s)"` + Name string `xorm:"UNIQUE(s) NOT NULL"` + CommitID string + CommitMessage string `xorm:"TEXT"` + PusherID int64 + Pusher *user_model.User `xorm:"-"` + IsDeleted bool `xorm:"index"` + DeletedByID int64 + DeletedBy *user_model.User `xorm:"-"` + DeletedUnix timeutil.TimeStamp `xorm:"index"` + CommitTime timeutil.TimeStamp // The commit + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` +} + +func (b *Branch) LoadDeletedBy(ctx context.Context) (err error) { + if b.DeletedBy == nil { + b.DeletedBy, err = user_model.GetUserByID(ctx, b.DeletedByID) + if user_model.IsErrUserNotExist(err) { + b.DeletedBy = user_model.NewGhostUser() + err = nil + } + } + return err +} + +func (b *Branch) LoadPusher(ctx context.Context) (err error) { + if b.Pusher == nil && b.PusherID > 0 { + b.Pusher, err = user_model.GetUserByID(ctx, b.PusherID) + if user_model.IsErrUserNotExist(err) { + b.Pusher = user_model.NewGhostUser() + err = nil + } + } + return err +} + +func init() { + db.RegisterModel(new(Branch)) + db.RegisterModel(new(RenamedBranch)) +} + +func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, error) { + var branch Branch + has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).And("name=?", branchName).Get(&branch) + if err != nil { + return nil, err + } else if !has { + return nil, ErrBranchNotExist{ + RepoID: repoID, + BranchName: branchName, + } + } + return &branch, nil +} + +func AddBranches(ctx context.Context, branches []*Branch) error { + for _, branch := range branches { + if _, err := db.GetEngine(ctx).Insert(branch); err != nil { + return err + } + } + return nil +} + +func GetDeletedBranchByID(ctx context.Context, repoID, branchID int64) (*Branch, error) { + var branch Branch + has, err := db.GetEngine(ctx).ID(branchID).Get(&branch) + if err != nil { + return nil, err + } else if !has { + return nil, ErrBranchNotExist{ + RepoID: repoID, + } + } + if branch.RepoID != repoID { + return nil, ErrBranchNotExist{ + RepoID: repoID, + } + } + if !branch.IsDeleted { + return nil, ErrBranchNotExist{ + RepoID: repoID, + } + } + return &branch, nil +} + +func DeleteBranches(ctx context.Context, repoID, doerID int64, branchIDs []int64) error { + return db.WithTx(ctx, func(ctx context.Context) error { + branches := make([]*Branch, 0, len(branchIDs)) + if err := db.GetEngine(ctx).In("id", branchIDs).Find(&branches); err != nil { + return err + } + for _, branch := range branches { + if err := AddDeletedBranch(ctx, repoID, branch.Name, doerID); err != nil { + return err + } + } + return nil + }) +} + +// UpdateBranch updates the branch information in the database. If the branch exist, it will update latest commit of this branch information +// If it doest not exist, insert a new record into database +func UpdateBranch(ctx context.Context, repoID int64, branchName, commitID, commitMessage string, pusherID int64, commitTime time.Time) error { + cnt, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repoID, branchName). + Cols("commit_id, commit_message, pusher_id, commit_time, is_deleted, updated_unix"). + Update(&Branch{ + CommitID: commitID, + CommitMessage: commitMessage, + PusherID: pusherID, + CommitTime: timeutil.TimeStamp(commitTime.Unix()), + IsDeleted: false, + }) + if err != nil { + return err + } + if cnt > 0 { + return nil + } + + return db.Insert(ctx, &Branch{ + RepoID: repoID, + Name: branchName, + CommitID: commitID, + CommitMessage: commitMessage, + PusherID: pusherID, + CommitTime: timeutil.TimeStamp(commitTime.Unix()), + }) +} + +// AddDeletedBranch adds a deleted branch to the database +func AddDeletedBranch(ctx context.Context, repoID int64, branchName string, deletedByID int64) error { + branch, err := GetBranch(ctx, repoID, branchName) + if err != nil { + return err + } + if branch.IsDeleted { + return nil + } + + cnt, err := db.GetEngine(ctx).Where("repo_id=? AND name=? AND is_deleted=?", repoID, branchName, false). + Cols("is_deleted, deleted_by_id, deleted_unix"). + Update(&Branch{ + IsDeleted: true, + DeletedByID: deletedByID, + DeletedUnix: timeutil.TimeStampNow(), + }) + if err != nil { + return err + } + if cnt == 0 { + return fmt.Errorf("branch %s not found or has been deleted", branchName) + } + return err +} + +func RemoveDeletedBranchByID(ctx context.Context, repoID, branchID int64) error { + _, err := db.GetEngine(ctx).Where("repo_id=? AND id=? AND is_deleted = ?", repoID, branchID, true).Delete(new(Branch)) + return err +} + +// RemoveOldDeletedBranches removes old deleted branches +func RemoveOldDeletedBranches(ctx context.Context, olderThan time.Duration) { + // Nothing to do for shutdown or terminate + log.Trace("Doing: DeletedBranchesCleanup") + + deleteBefore := time.Now().Add(-olderThan) + _, err := db.GetEngine(ctx).Where("is_deleted=? AND deleted_unix < ?", true, deleteBefore.Unix()).Delete(new(Branch)) + if err != nil { + log.Error("DeletedBranchesCleanup: %v", err) + } +} + +// RenamedBranch provide renamed branch log +// will check it when a branch can't be found +type RenamedBranch struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX NOT NULL"` + From string + To string + CreatedUnix timeutil.TimeStamp `xorm:"created"` +} + +// FindRenamedBranch check if a branch was renamed +func FindRenamedBranch(ctx context.Context, repoID int64, from string) (branch *RenamedBranch, exist bool, err error) { + branch = &RenamedBranch{ + RepoID: repoID, + From: from, + } + exist, err = db.GetEngine(ctx).Get(branch) + + return branch, exist, err +} + +// RenameBranch rename a branch +func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to string, gitAction func(isDefault bool) error) (err error) { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + sess := db.GetEngine(ctx) + + // 1. update branch in database + if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Update(&Branch{ + Name: to, + }); err != nil { + return err + } else if n <= 0 { + return ErrBranchNotExist{ + RepoID: repo.ID, + BranchName: from, + } + } + + // 2. update default branch if needed + isDefault := repo.DefaultBranch == from + if isDefault { + repo.DefaultBranch = to + _, err = sess.ID(repo.ID).Cols("default_branch").Update(repo) + if err != nil { + return err + } + } + + // 3. Update protected branch if needed + protectedBranch, err := GetProtectedBranchRuleByName(ctx, repo.ID, from) + if err != nil { + return err + } + + if protectedBranch != nil { + // there is a protect rule for this branch + protectedBranch.RuleName = to + _, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch) + if err != nil { + return err + } + } else { + // some glob protect rules may match this branch + protected, err := IsBranchProtected(ctx, repo.ID, from) + if err != nil { + return err + } + if protected { + return ErrBranchIsProtected + } + } + + // 4. Update all not merged pull request base branch name + _, err = sess.Table("pull_request").Where("base_repo_id=? AND base_branch=? AND has_merged=?", + repo.ID, from, false). + Update(map[string]interface{}{"base_branch": to}) + if err != nil { + return err + } + + // 5. do git action + if err = gitAction(isDefault); err != nil { + return err + } + + // 6. insert renamed branch record + renamedBranch := &RenamedBranch{ + RepoID: repo.ID, + From: from, + To: to, + } + err = db.Insert(ctx, renamedBranch) + if err != nil { + return err + } + + return committer.Commit() +} diff --git a/models/git/branch_list.go b/models/git/branch_list.go new file mode 100644 index 0000000000..da78248c0b --- /dev/null +++ b/models/git/branch_list.go @@ -0,0 +1,132 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "context" + + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" + "xorm.io/xorm" +) + +type BranchList []*Branch + +func (branches BranchList) LoadDeletedBy(ctx context.Context) error { + ids := container.Set[int64]{} + for _, branch := range branches { + if !branch.IsDeleted { + continue + } + ids.Add(branch.DeletedByID) + } + usersMap := make(map[int64]*user_model.User, len(ids)) + if err := db.GetEngine(ctx).In("id", ids.Values()).Find(&usersMap); err != nil { + return err + } + for _, branch := range branches { + if !branch.IsDeleted { + continue + } + branch.DeletedBy = usersMap[branch.DeletedByID] + if branch.DeletedBy == nil { + branch.DeletedBy = user_model.NewGhostUser() + } + } + return nil +} + +func (branches BranchList) LoadPusher(ctx context.Context) error { + ids := container.Set[int64]{} + for _, branch := range branches { + if branch.PusherID > 0 { // pusher_id maybe zero because some branches are sync by backend with no pusher + ids.Add(branch.PusherID) + } + } + usersMap := make(map[int64]*user_model.User, len(ids)) + if err := db.GetEngine(ctx).In("id", ids.Values()).Find(&usersMap); err != nil { + return err + } + for _, branch := range branches { + if branch.PusherID <= 0 { + continue + } + branch.Pusher = usersMap[branch.PusherID] + if branch.Pusher == nil { + branch.Pusher = user_model.NewGhostUser() + } + } + return nil +} + +const ( + BranchOrderByNameAsc = "name ASC" + BranchOrderByCommitTimeDesc = "commit_time DESC" +) + +type FindBranchOptions struct { + db.ListOptions + RepoID int64 + ExcludeBranchNames []string + IsDeletedBranch util.OptionalBool + OrderBy string +} + +func (opts *FindBranchOptions) Cond() builder.Cond { + cond := builder.NewCond() + if opts.RepoID > 0 { + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + } + + if len(opts.ExcludeBranchNames) > 0 { + cond = cond.And(builder.NotIn("name", opts.ExcludeBranchNames)) + } + if !opts.IsDeletedBranch.IsNone() { + cond = cond.And(builder.Eq{"is_deleted": opts.IsDeletedBranch.IsTrue()}) + } + return cond +} + +func CountBranches(ctx context.Context, opts FindBranchOptions) (int64, error) { + return db.GetEngine(ctx).Where(opts.Cond()).Count(&Branch{}) +} + +func orderByBranches(sess *xorm.Session, opts FindBranchOptions) *xorm.Session { + if !opts.IsDeletedBranch.IsFalse() { // if deleted branch included, put them at the end + sess = sess.OrderBy("is_deleted ASC") + } + + if opts.OrderBy == "" { + opts.OrderBy = BranchOrderByCommitTimeDesc + } + return sess.OrderBy(opts.OrderBy) +} + +func FindBranches(ctx context.Context, opts FindBranchOptions) (BranchList, error) { + sess := db.GetEngine(ctx).Where(opts.Cond()) + if opts.PageSize > 0 && !opts.IsListAll() { + sess = db.SetSessionPagination(sess, &opts.ListOptions) + } + sess = orderByBranches(sess, opts) + + var branches []*Branch + return branches, sess.Find(&branches) +} + +func FindBranchNames(ctx context.Context, opts FindBranchOptions) ([]string, error) { + sess := db.GetEngine(ctx).Select("name").Where(opts.Cond()) + if opts.PageSize > 0 && !opts.IsListAll() { + sess = db.SetSessionPagination(sess, &opts.ListOptions) + } + sess = orderByBranches(sess, opts) + var branches []string + if err := sess.Table("branch").Find(&branches); err != nil { + return nil, err + } + return branches, nil +} diff --git a/models/git/branch_test.go b/models/git/branch_test.go new file mode 100644 index 0000000000..bb63660d07 --- /dev/null +++ b/models/git/branch_test.go @@ -0,0 +1,176 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" + issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/util" + + "github.com/stretchr/testify/assert" +) + +func TestAddDeletedBranch(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 1}) + + assert.True(t, firstBranch.IsDeleted) + assert.NoError(t, git_model.AddDeletedBranch(db.DefaultContext, repo.ID, firstBranch.Name, firstBranch.DeletedByID)) + assert.NoError(t, git_model.AddDeletedBranch(db.DefaultContext, repo.ID, "branch2", int64(1))) + + secondBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo.ID, Name: "branch2"}) + assert.True(t, secondBranch.IsDeleted) + + err := git_model.UpdateBranch(db.DefaultContext, repo.ID, secondBranch.Name, secondBranch.CommitID, secondBranch.CommitMessage, secondBranch.PusherID, secondBranch.CommitTime.AsLocalTime()) + assert.NoError(t, err) +} + +func TestGetDeletedBranches(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + branches, err := git_model.FindBranches(db.DefaultContext, git_model.FindBranchOptions{ + ListOptions: db.ListOptions{ + ListAll: true, + }, + RepoID: repo.ID, + IsDeletedBranch: util.OptionalBoolTrue, + }) + assert.NoError(t, err) + assert.Len(t, branches, 2) +} + +func TestGetDeletedBranch(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 1}) + + assert.NotNil(t, getDeletedBranch(t, firstBranch)) +} + +func TestDeletedBranchLoadUser(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 1}) + secondBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 2}) + + branch := getDeletedBranch(t, firstBranch) + assert.Nil(t, branch.DeletedBy) + branch.LoadDeletedBy(db.DefaultContext) + assert.NotNil(t, branch.DeletedBy) + assert.Equal(t, "user1", branch.DeletedBy.Name) + + branch = getDeletedBranch(t, secondBranch) + assert.Nil(t, branch.DeletedBy) + branch.LoadDeletedBy(db.DefaultContext) + assert.NotNil(t, branch.DeletedBy) + assert.Equal(t, "Ghost", branch.DeletedBy.Name) +} + +func TestRemoveDeletedBranch(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 1}) + + err := git_model.RemoveDeletedBranchByID(db.DefaultContext, repo.ID, 1) + assert.NoError(t, err) + unittest.AssertNotExistsBean(t, firstBranch) + unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 2}) +} + +func getDeletedBranch(t *testing.T, branch *git_model.Branch) *git_model.Branch { + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + deletedBranch, err := git_model.GetDeletedBranchByID(db.DefaultContext, repo.ID, branch.ID) + assert.NoError(t, err) + assert.Equal(t, branch.ID, deletedBranch.ID) + assert.Equal(t, branch.Name, deletedBranch.Name) + assert.Equal(t, branch.CommitID, deletedBranch.CommitID) + assert.Equal(t, branch.DeletedByID, deletedBranch.DeletedByID) + + return deletedBranch +} + +func TestFindRenamedBranch(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + branch, exist, err := git_model.FindRenamedBranch(db.DefaultContext, 1, "dev") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, "master", branch.To) + + _, exist, err = git_model.FindRenamedBranch(db.DefaultContext, 1, "unknow") + assert.NoError(t, err) + assert.False(t, exist) +} + +func TestRenameBranch(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + _isDefault := false + + ctx, committer, err := db.TxContext(db.DefaultContext) + defer committer.Close() + assert.NoError(t, err) + assert.NoError(t, git_model.UpdateProtectBranch(ctx, repo1, &git_model.ProtectedBranch{ + RepoID: repo1.ID, + RuleName: "master", + }, git_model.WhitelistOptions{})) + assert.NoError(t, committer.Commit()) + + assert.NoError(t, git_model.RenameBranch(db.DefaultContext, repo1, "master", "main", func(isDefault bool) error { + _isDefault = isDefault + return nil + })) + + assert.True(t, _isDefault) + repo1 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + assert.Equal(t, "main", repo1.DefaultBranch) + + pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) // merged + assert.Equal(t, "master", pull.BaseBranch) + + pull = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) // open + assert.Equal(t, "main", pull.BaseBranch) + + renamedBranch := unittest.AssertExistsAndLoadBean(t, &git_model.RenamedBranch{ID: 2}) + assert.Equal(t, "master", renamedBranch.From) + assert.Equal(t, "main", renamedBranch.To) + assert.Equal(t, int64(1), renamedBranch.RepoID) + + unittest.AssertExistsAndLoadBean(t, &git_model.ProtectedBranch{ + RepoID: repo1.ID, + RuleName: "main", + }) +} + +func TestOnlyGetDeletedBranchOnCorrectRepo(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // Get deletedBranch with ID of 1 on repo with ID 2. + // This should return a nil branch as this deleted branch + // is actually on repo with ID 1. + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + + deletedBranch, err := git_model.GetDeletedBranchByID(db.DefaultContext, repo2.ID, 1) + + // Expect error, and the returned branch is nil. + assert.Error(t, err) + assert.Nil(t, deletedBranch) + + // Now get the deletedBranch with ID of 1 on repo with ID 1. + // This should return the deletedBranch. + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + deletedBranch, err = git_model.GetDeletedBranchByID(db.DefaultContext, repo1.ID, 1) + + // Expect no error, and the returned branch to be not nil. + assert.NoError(t, err) + assert.NotNil(t, deletedBranch) +} diff --git a/models/git/branches.go b/models/git/branches.go deleted file mode 100644 index b94ea32959..0000000000 --- a/models/git/branches.go +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright 2016 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package git - -import ( - "context" - "fmt" - "time" - - "code.gitea.io/gitea/models/db" - repo_model "code.gitea.io/gitea/models/repo" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/timeutil" -) - -// DeletedBranch struct -type DeletedBranch struct { - ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` - Name string `xorm:"UNIQUE(s) NOT NULL"` - Commit string `xorm:"UNIQUE(s) NOT NULL"` - DeletedByID int64 `xorm:"INDEX"` - DeletedBy *user_model.User `xorm:"-"` - DeletedUnix timeutil.TimeStamp `xorm:"INDEX created"` -} - -func init() { - db.RegisterModel(new(DeletedBranch)) - db.RegisterModel(new(RenamedBranch)) -} - -// AddDeletedBranch adds a deleted branch to the database -func AddDeletedBranch(ctx context.Context, repoID int64, branchName, commit string, deletedByID int64) error { - deletedBranch := &DeletedBranch{ - RepoID: repoID, - Name: branchName, - Commit: commit, - DeletedByID: deletedByID, - } - - _, err := db.GetEngine(ctx).Insert(deletedBranch) - return err -} - -// GetDeletedBranches returns all the deleted branches -func GetDeletedBranches(ctx context.Context, repoID int64) ([]*DeletedBranch, error) { - deletedBranches := make([]*DeletedBranch, 0) - return deletedBranches, db.GetEngine(ctx).Where("repo_id = ?", repoID).Desc("deleted_unix").Find(&deletedBranches) -} - -// GetDeletedBranchByID get a deleted branch by its ID -func GetDeletedBranchByID(ctx context.Context, repoID, id int64) (*DeletedBranch, error) { - deletedBranch := &DeletedBranch{} - has, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).And("id = ?", id).Get(deletedBranch) - if err != nil { - return nil, err - } - if !has { - return nil, nil - } - return deletedBranch, nil -} - -// RemoveDeletedBranchByID removes a deleted branch from the database -func RemoveDeletedBranchByID(ctx context.Context, repoID, id int64) (err error) { - deletedBranch := &DeletedBranch{ - RepoID: repoID, - ID: id, - } - - if affected, err := db.GetEngine(ctx).Delete(deletedBranch); err != nil { - return err - } else if affected != 1 { - return fmt.Errorf("remove deleted branch ID(%v) failed", id) - } - - return nil -} - -// LoadUser loads the user that deleted the branch -// When there's no user found it returns a user_model.NewGhostUser -func (deletedBranch *DeletedBranch) LoadUser(ctx context.Context) { - user, err := user_model.GetUserByID(ctx, deletedBranch.DeletedByID) - if err != nil { - user = user_model.NewGhostUser() - } - deletedBranch.DeletedBy = user -} - -// RemoveDeletedBranchByName removes all deleted branches -func RemoveDeletedBranchByName(ctx context.Context, repoID int64, branch string) error { - _, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repoID, branch).Delete(new(DeletedBranch)) - return err -} - -// RemoveOldDeletedBranches removes old deleted branches -func RemoveOldDeletedBranches(ctx context.Context, olderThan time.Duration) { - // Nothing to do for shutdown or terminate - log.Trace("Doing: DeletedBranchesCleanup") - - deleteBefore := time.Now().Add(-olderThan) - _, err := db.GetEngine(ctx).Where("deleted_unix < ?", deleteBefore.Unix()).Delete(new(DeletedBranch)) - if err != nil { - log.Error("DeletedBranchesCleanup: %v", err) - } -} - -// RenamedBranch provide renamed branch log -// will check it when a branch can't be found -type RenamedBranch struct { - ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"INDEX NOT NULL"` - From string - To string - CreatedUnix timeutil.TimeStamp `xorm:"created"` -} - -// FindRenamedBranch check if a branch was renamed -func FindRenamedBranch(ctx context.Context, repoID int64, from string) (branch *RenamedBranch, exist bool, err error) { - branch = &RenamedBranch{ - RepoID: repoID, - From: from, - } - exist, err = db.GetEngine(ctx).Get(branch) - - return branch, exist, err -} - -// RenameBranch rename a branch -func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to string, gitAction func(isDefault bool) error) (err error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - sess := db.GetEngine(ctx) - // 1. update default branch if needed - isDefault := repo.DefaultBranch == from - if isDefault { - repo.DefaultBranch = to - _, err = sess.ID(repo.ID).Cols("default_branch").Update(repo) - if err != nil { - return err - } - } - - // 2. Update protected branch if needed - protectedBranch, err := GetProtectedBranchRuleByName(ctx, repo.ID, from) - if err != nil { - return err - } - - if protectedBranch != nil { - protectedBranch.RuleName = to - _, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch) - if err != nil { - return err - } - } else { - protected, err := IsBranchProtected(ctx, repo.ID, from) - if err != nil { - return err - } - if protected { - return ErrBranchIsProtected - } - } - - // 3. Update all not merged pull request base branch name - _, err = sess.Table("pull_request").Where("base_repo_id=? AND base_branch=? AND has_merged=?", - repo.ID, from, false). - Update(map[string]interface{}{"base_branch": to}) - if err != nil { - return err - } - - // 4. do git action - if err = gitAction(isDefault); err != nil { - return err - } - - // 5. insert renamed branch record - renamedBranch := &RenamedBranch{ - RepoID: repo.ID, - From: from, - To: to, - } - err = db.Insert(ctx, renamedBranch) - if err != nil { - return err - } - - return committer.Commit() -} diff --git a/models/git/branches_test.go b/models/git/branches_test.go deleted file mode 100644 index 5d18d9525e..0000000000 --- a/models/git/branches_test.go +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package git_test - -import ( - "testing" - - "code.gitea.io/gitea/models/db" - git_model "code.gitea.io/gitea/models/git" - issues_model "code.gitea.io/gitea/models/issues" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unittest" - - "github.com/stretchr/testify/assert" -) - -func TestAddDeletedBranch(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.DeletedBranch{ID: 1}) - - assert.Error(t, git_model.AddDeletedBranch(db.DefaultContext, repo.ID, firstBranch.Name, firstBranch.Commit, firstBranch.DeletedByID)) - assert.NoError(t, git_model.AddDeletedBranch(db.DefaultContext, repo.ID, "test", "5655464564554545466464656", int64(1))) -} - -func TestGetDeletedBranches(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - - branches, err := git_model.GetDeletedBranches(db.DefaultContext, repo.ID) - assert.NoError(t, err) - assert.Len(t, branches, 2) -} - -func TestGetDeletedBranch(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.DeletedBranch{ID: 1}) - - assert.NotNil(t, getDeletedBranch(t, firstBranch)) -} - -func TestDeletedBranchLoadUser(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.DeletedBranch{ID: 1}) - secondBranch := unittest.AssertExistsAndLoadBean(t, &git_model.DeletedBranch{ID: 2}) - - branch := getDeletedBranch(t, firstBranch) - assert.Nil(t, branch.DeletedBy) - branch.LoadUser(db.DefaultContext) - assert.NotNil(t, branch.DeletedBy) - assert.Equal(t, "user1", branch.DeletedBy.Name) - - branch = getDeletedBranch(t, secondBranch) - assert.Nil(t, branch.DeletedBy) - branch.LoadUser(db.DefaultContext) - assert.NotNil(t, branch.DeletedBy) - assert.Equal(t, "Ghost", branch.DeletedBy.Name) -} - -func TestRemoveDeletedBranch(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - - firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.DeletedBranch{ID: 1}) - - err := git_model.RemoveDeletedBranchByID(db.DefaultContext, repo.ID, 1) - assert.NoError(t, err) - unittest.AssertNotExistsBean(t, firstBranch) - unittest.AssertExistsAndLoadBean(t, &git_model.DeletedBranch{ID: 2}) -} - -func getDeletedBranch(t *testing.T, branch *git_model.DeletedBranch) *git_model.DeletedBranch { - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - - deletedBranch, err := git_model.GetDeletedBranchByID(db.DefaultContext, repo.ID, branch.ID) - assert.NoError(t, err) - assert.Equal(t, branch.ID, deletedBranch.ID) - assert.Equal(t, branch.Name, deletedBranch.Name) - assert.Equal(t, branch.Commit, deletedBranch.Commit) - assert.Equal(t, branch.DeletedByID, deletedBranch.DeletedByID) - - return deletedBranch -} - -func TestFindRenamedBranch(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - branch, exist, err := git_model.FindRenamedBranch(db.DefaultContext, 1, "dev") - assert.NoError(t, err) - assert.True(t, exist) - assert.Equal(t, "master", branch.To) - - _, exist, err = git_model.FindRenamedBranch(db.DefaultContext, 1, "unknow") - assert.NoError(t, err) - assert.False(t, exist) -} - -func TestRenameBranch(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - _isDefault := false - - ctx, committer, err := db.TxContext(db.DefaultContext) - defer committer.Close() - assert.NoError(t, err) - assert.NoError(t, git_model.UpdateProtectBranch(ctx, repo1, &git_model.ProtectedBranch{ - RepoID: repo1.ID, - RuleName: "master", - }, git_model.WhitelistOptions{})) - assert.NoError(t, committer.Commit()) - - assert.NoError(t, git_model.RenameBranch(db.DefaultContext, repo1, "master", "main", func(isDefault bool) error { - _isDefault = isDefault - return nil - })) - - assert.True(t, _isDefault) - repo1 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - assert.Equal(t, "main", repo1.DefaultBranch) - - pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) // merged - assert.Equal(t, "master", pull.BaseBranch) - - pull = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) // open - assert.Equal(t, "main", pull.BaseBranch) - - renamedBranch := unittest.AssertExistsAndLoadBean(t, &git_model.RenamedBranch{ID: 2}) - assert.Equal(t, "master", renamedBranch.From) - assert.Equal(t, "main", renamedBranch.To) - assert.Equal(t, int64(1), renamedBranch.RepoID) - - unittest.AssertExistsAndLoadBean(t, &git_model.ProtectedBranch{ - RepoID: repo1.ID, - RuleName: "main", - }) -} - -func TestOnlyGetDeletedBranchOnCorrectRepo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - // Get deletedBranch with ID of 1 on repo with ID 2. - // This should return a nil branch as this deleted branch - // is actually on repo with ID 1. - repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) - - deletedBranch, err := git_model.GetDeletedBranchByID(db.DefaultContext, repo2.ID, 1) - - // Expect no error, and the returned branch is nil. - assert.NoError(t, err) - assert.Nil(t, deletedBranch) - - // Now get the deletedBranch with ID of 1 on repo with ID 1. - // This should return the deletedBranch. - repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - - deletedBranch, err = git_model.GetDeletedBranchByID(db.DefaultContext, repo1.ID, 1) - - // Expect no error, and the returned branch to be not nil. - assert.NoError(t, err) - assert.NotNil(t, deletedBranch) -} diff --git a/models/git/protected_branch_list.go b/models/git/protected_branch_list.go index 17fe6d701f..eeb307e245 100644 --- a/models/git/protected_branch_list.go +++ b/models/git/protected_branch_list.go @@ -8,7 +8,7 @@ import ( "sort" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/util" "github.com/gobwas/glob" ) @@ -47,19 +47,32 @@ func FindRepoProtectedBranchRules(ctx context.Context, repoID int64) (ProtectedB } // FindAllMatchedBranches find all matched branches -func FindAllMatchedBranches(ctx context.Context, gitRepo *git.Repository, ruleName string) ([]string, error) { - // FIXME: how many should we get? - branches, _, err := gitRepo.GetBranchNames(0, 9999999) - if err != nil { - return nil, err - } - rule := glob.MustCompile(ruleName) - results := make([]string, 0, len(branches)) - for _, branch := range branches { - if rule.Match(branch) { - results = append(results, branch) +func FindAllMatchedBranches(ctx context.Context, repoID int64, ruleName string) ([]string, error) { + results := make([]string, 0, 10) + for page := 1; ; page++ { + brancheNames, err := FindBranchNames(ctx, FindBranchOptions{ + ListOptions: db.ListOptions{ + PageSize: 100, + Page: page, + }, + RepoID: repoID, + IsDeletedBranch: util.OptionalBoolFalse, + }) + if err != nil { + return nil, err + } + rule := glob.MustCompile(ruleName) + + for _, branch := range brancheNames { + if rule.Match(branch) { + results = append(results, branch) + } + } + if len(brancheNames) < 100 { + break } } + return results, nil } -- cgit v1.2.3