mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	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 <i@wolfogre.com>
This commit is contained in:
		| @@ -318,90 +318,6 @@ func (err ErrFilePathProtected) Unwrap() error { | ||||
| 	return util.ErrPermissionDenied | ||||
| } | ||||
|  | ||||
| // __________                             .__ | ||||
| // \______   \____________    ____   ____ |  |__ | ||||
| //  |    |  _/\_  __ \__  \  /    \_/ ___\|  |  \ | ||||
| //  |    |   \ |  | \// __ \|   |  \  \___|   Y  \ | ||||
| //  |______  / |__|  (____  /___|  /\___  >___|  / | ||||
| //         \/             \/     \/     \/     \/ | ||||
|  | ||||
| // ErrBranchDoesNotExist represents an error that branch with such name does not exist. | ||||
| type ErrBranchDoesNotExist struct { | ||||
| 	BranchName string | ||||
| } | ||||
|  | ||||
| // IsErrBranchDoesNotExist checks if an error is an ErrBranchDoesNotExist. | ||||
| func IsErrBranchDoesNotExist(err error) bool { | ||||
| 	_, ok := err.(ErrBranchDoesNotExist) | ||||
| 	return ok | ||||
| } | ||||
|  | ||||
| func (err ErrBranchDoesNotExist) Error() string { | ||||
| 	return fmt.Sprintf("branch does not exist [name: %s]", err.BranchName) | ||||
| } | ||||
|  | ||||
| func (err ErrBranchDoesNotExist) 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 branch name conflicts with other 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 | ||||
| } | ||||
|  | ||||
| // ErrDisallowedToMerge represents an error that a branch is protected and the current user is not allowed to modify it. | ||||
| type ErrDisallowedToMerge struct { | ||||
| 	Reason string | ||||
|   | ||||
							
								
								
									
										47
									
								
								models/fixtures/branch.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								models/fixtures/branch.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| - | ||||
|   id: 1 | ||||
|   repo_id: 1 | ||||
|   name: 'foo' | ||||
|   commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d' | ||||
|   commit_message: 'first commit' | ||||
|   commit_time: 978307100 | ||||
|   pusher_id: 1 | ||||
|   is_deleted: true | ||||
|   deleted_by_id: 1 | ||||
|   deleted_unix: 978307200 | ||||
|  | ||||
| - | ||||
|   id: 2 | ||||
|   repo_id: 1 | ||||
|   name: 'bar' | ||||
|   commit_id: '62fb502a7172d4453f0322a2cc85bddffa57f07a' | ||||
|   commit_message: 'second commit' | ||||
|   commit_time: 978307100 | ||||
|   pusher_id: 1 | ||||
|   is_deleted: true | ||||
|   deleted_by_id: 99 | ||||
|   deleted_unix: 978307200 | ||||
|  | ||||
| - | ||||
|   id: 3 | ||||
|   repo_id: 1 | ||||
|   name: 'branch2' | ||||
|   commit_id: '985f0301dba5e7b34be866819cd15ad3d8f508ee' | ||||
|   commit_message: 'make pull5 outdated' | ||||
|   commit_time: 1579166279 | ||||
|   pusher_id: 1 | ||||
|   is_deleted: false | ||||
|   deleted_by_id: 0 | ||||
|   deleted_unix: 0 | ||||
|  | ||||
| - | ||||
|   id: 4 | ||||
|   repo_id: 1 | ||||
|   name: 'master' | ||||
|   commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d' | ||||
|   commit_message: 'Initial commit' | ||||
|   commit_time: 1489927679 | ||||
|   pusher_id: 1 | ||||
|   is_deleted: false | ||||
|   deleted_by_id: 0 | ||||
|   deleted_unix: 0 | ||||
| @@ -1,15 +0,0 @@ | ||||
| - | ||||
|   id: 1 | ||||
|   repo_id: 1 | ||||
|   name: foo | ||||
|   commit: 1213212312313213213132131 | ||||
|   deleted_by_id: 1 | ||||
|   deleted_unix: 978307200 | ||||
|  | ||||
| - | ||||
|   id: 2 | ||||
|   repo_id: 1 | ||||
|   name: bar | ||||
|   commit: 5655464564554545466464655 | ||||
|   deleted_by_id: 99 | ||||
|   deleted_unix: 978307200 | ||||
							
								
								
									
										379
									
								
								models/git/branch.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										379
									
								
								models/git/branch.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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() | ||||
| } | ||||
							
								
								
									
										132
									
								
								models/git/branch_list.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								models/git/branch_list.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
| @@ -11,6 +11,7 @@ import ( | ||||
| 	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" | ||||
| ) | ||||
| @@ -18,24 +19,37 @@ import ( | ||||
| 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}) | ||||
| 	firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{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))) | ||||
| 	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.GetDeletedBranches(db.DefaultContext, repo.ID) | ||||
| 	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.DeletedBranch{ID: 1}) | ||||
| 	firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 1}) | ||||
| 
 | ||||
| 	assert.NotNil(t, getDeletedBranch(t, firstBranch)) | ||||
| } | ||||
| @@ -43,18 +57,18 @@ func TestGetDeletedBranch(t *testing.T) { | ||||
| 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}) | ||||
| 	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.LoadUser(db.DefaultContext) | ||||
| 	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.LoadUser(db.DefaultContext) | ||||
| 	branch.LoadDeletedBy(db.DefaultContext) | ||||
| 	assert.NotNil(t, branch.DeletedBy) | ||||
| 	assert.Equal(t, "Ghost", branch.DeletedBy.Name) | ||||
| } | ||||
| @@ -63,22 +77,22 @@ 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}) | ||||
| 	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.DeletedBranch{ID: 2}) | ||||
| 	unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 2}) | ||||
| } | ||||
| 
 | ||||
| func getDeletedBranch(t *testing.T, branch *git_model.DeletedBranch) *git_model.DeletedBranch { | ||||
| 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.Commit, deletedBranch.Commit) | ||||
| 	assert.Equal(t, branch.CommitID, deletedBranch.CommitID) | ||||
| 	assert.Equal(t, branch.DeletedByID, deletedBranch.DeletedByID) | ||||
| 
 | ||||
| 	return deletedBranch | ||||
| @@ -146,8 +160,8 @@ func TestOnlyGetDeletedBranchOnCorrectRepo(t *testing.T) { | ||||
| 
 | ||||
| 	deletedBranch, err := git_model.GetDeletedBranchByID(db.DefaultContext, repo2.ID, 1) | ||||
| 
 | ||||
| 	// Expect no error, and the returned branch is nil. | ||||
| 	assert.NoError(t, err) | ||||
| 	// 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. | ||||
| @@ -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() | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -509,6 +509,8 @@ var migrations = []Migration{ | ||||
| 	NewMigration("Add TriggerEvent to action_run table", v1_21.AddTriggerEventToActionRun), | ||||
| 	// v263 -> v264 | ||||
| 	NewMigration("Add git_size and lfs_size columns to repository table", v1_21.AddGitSizeAndLFSSizeToRepositoryTable), | ||||
| 	// v264 -> v265 | ||||
| 	NewMigration("Add branch table", v1_21.AddBranchTable), | ||||
| } | ||||
|  | ||||
| // GetCurrentDBVersion returns the current db version | ||||
|   | ||||
							
								
								
									
										93
									
								
								models/migrations/v1_21/v264.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								models/migrations/v1_21/v264.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | ||||
| // Copyright 2023 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
|  | ||||
| package v1_21 //nolint | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
|  | ||||
| 	"xorm.io/xorm" | ||||
| ) | ||||
|  | ||||
| func AddBranchTable(x *xorm.Engine) error { | ||||
| 	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 | ||||
| 		IsDeleted     bool `xorm:"index"` | ||||
| 		DeletedByID   int64 | ||||
| 		DeletedUnix   timeutil.TimeStamp `xorm:"index"` | ||||
| 		CommitTime    timeutil.TimeStamp // The commit | ||||
| 		CreatedUnix   timeutil.TimeStamp `xorm:"created"` | ||||
| 		UpdatedUnix   timeutil.TimeStamp `xorm:"updated"` | ||||
| 	} | ||||
|  | ||||
| 	if err := x.Sync(new(Branch)); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if exist, err := x.IsTableExist("deleted_branches"); err != nil { | ||||
| 		return err | ||||
| 	} else if !exist { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	type DeletedBranch struct { | ||||
| 		ID          int64 | ||||
| 		RepoID      int64  `xorm:"index UNIQUE(s)"` | ||||
| 		Name        string `xorm:"UNIQUE(s) NOT NULL"` | ||||
| 		Commit      string | ||||
| 		DeletedByID int64 | ||||
| 		DeletedUnix timeutil.TimeStamp | ||||
| 	} | ||||
|  | ||||
| 	var adminUserID int64 | ||||
| 	has, err := x.Table("user"). | ||||
| 		Select("id"). | ||||
| 		Where("is_admin=?", true). | ||||
| 		Asc("id"). // Reliably get the admin with the lowest ID. | ||||
| 		Get(&adminUserID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} else if !has { | ||||
| 		return fmt.Errorf("no admin user found") | ||||
| 	} | ||||
|  | ||||
| 	branches := make([]Branch, 0, 100) | ||||
| 	if err := db.Iterate(context.Background(), nil, func(ctx context.Context, deletedBranch *DeletedBranch) error { | ||||
| 		branches = append(branches, Branch{ | ||||
| 			RepoID:      deletedBranch.RepoID, | ||||
| 			Name:        deletedBranch.Name, | ||||
| 			CommitID:    deletedBranch.Commit, | ||||
| 			PusherID:    adminUserID, | ||||
| 			IsDeleted:   true, | ||||
| 			DeletedByID: deletedBranch.DeletedByID, | ||||
| 			DeletedUnix: deletedBranch.DeletedUnix, | ||||
| 		}) | ||||
| 		if len(branches) >= 100 { | ||||
| 			_, err := x.Insert(&branches) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			branches = branches[:0] | ||||
| 		} | ||||
| 		return nil | ||||
| 	}); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if len(branches) > 0 { | ||||
| 		if _, err := x.Insert(&branches); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return x.DropTables("deleted_branches") | ||||
| } | ||||
| @@ -147,7 +147,7 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { | ||||
| 		&repo_model.Collaboration{RepoID: repoID}, | ||||
| 		&issues_model.Comment{RefRepoID: repoID}, | ||||
| 		&git_model.CommitStatus{RepoID: repoID}, | ||||
| 		&git_model.DeletedBranch{RepoID: repoID}, | ||||
| 		&git_model.Branch{RepoID: repoID}, | ||||
| 		&git_model.LFSLock{RepoID: repoID}, | ||||
| 		&repo_model.LanguageStat{RepoID: repoID}, | ||||
| 		&issues_model.Milestone{RepoID: repoID}, | ||||
|   | ||||
| @@ -1171,9 +1171,9 @@ func GetUserByOpenID(uri string) (*User, error) { | ||||
| } | ||||
|  | ||||
| // GetAdminUser returns the first administrator | ||||
| func GetAdminUser() (*User, error) { | ||||
| func GetAdminUser(ctx context.Context) (*User, error) { | ||||
| 	var admin User | ||||
| 	has, err := db.GetEngine(db.DefaultContext). | ||||
| 	has, err := db.GetEngine(ctx). | ||||
| 		Where("is_admin=?", true). | ||||
| 		Asc("id"). // Reliably get the admin with the lowest ID. | ||||
| 		Get(&admin) | ||||
|   | ||||
| @@ -667,13 +667,38 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { | ||||
| 	} | ||||
| 	ctx.Data["Tags"] = tags | ||||
|  | ||||
| 	brs, _, err := ctx.Repo.GitRepo.GetBranchNames(0, 0) | ||||
| 	branchOpts := git_model.FindBranchOptions{ | ||||
| 		RepoID:          ctx.Repo.Repository.ID, | ||||
| 		IsDeletedBranch: util.OptionalBoolFalse, | ||||
| 		ListOptions: db.ListOptions{ | ||||
| 			ListAll: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	branchesTotal, err := git_model.CountBranches(ctx, branchOpts) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("CountBranches", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// non empty repo should have at least 1 branch, so this repository's branches haven't been synced yet | ||||
| 	if branchesTotal == 0 { // fallback to do a sync immediately | ||||
| 		branchesTotal, err = repo_module.SyncRepoBranches(ctx, ctx.Repo.Repository.ID, 0) | ||||
| 		if err != nil { | ||||
| 			ctx.ServerError("SyncRepoBranches", err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// FIXME: use paganation and async loading | ||||
| 	branchOpts.ExcludeBranchNames = []string{ctx.Repo.Repository.DefaultBranch} | ||||
| 	brs, err := git_model.FindBranchNames(ctx, branchOpts) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("GetBranches", err) | ||||
| 		return | ||||
| 	} | ||||
| 	ctx.Data["Branches"] = brs | ||||
| 	ctx.Data["BranchesCount"] = len(brs) | ||||
| 	// always put default branch on the top | ||||
| 	ctx.Data["Branches"] = append(branchOpts.ExcludeBranchNames, brs...) | ||||
| 	ctx.Data["BranchesCount"] = branchesTotal | ||||
|  | ||||
| 	// If not branch selected, try default one. | ||||
| 	// If default branch doesn't exist, fall back to some other branch. | ||||
| @@ -897,9 +922,9 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context | ||||
| 		if len(ctx.Params("*")) == 0 { | ||||
| 			refName = ctx.Repo.Repository.DefaultBranch | ||||
| 			if !ctx.Repo.GitRepo.IsBranchExist(refName) { | ||||
| 				brs, _, err := ctx.Repo.GitRepo.GetBranchNames(0, 0) | ||||
| 				brs, _, err := ctx.Repo.GitRepo.GetBranches(0, 1) | ||||
| 				if err == nil && len(brs) != 0 { | ||||
| 					refName = brs[0] | ||||
| 					refName = brs[0].Name | ||||
| 				} else if len(brs) == 0 { | ||||
| 					log.Error("No branches in non-empty repository %s", ctx.Repo.GitRepo.Path) | ||||
| 					ctx.Repo.Repository.MarkAsBrokenEmpty() | ||||
|   | ||||
							
								
								
									
										135
									
								
								modules/repository/branch.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								modules/repository/branch.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | ||||
| // Copyright 2023 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
|  | ||||
| package repository | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	git_model "code.gitea.io/gitea/models/git" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	"code.gitea.io/gitea/modules/container" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| ) | ||||
|  | ||||
| // SyncRepoBranches synchronizes branch table with repository branches | ||||
| func SyncRepoBranches(ctx context.Context, repoID, doerID int64) (int64, error) { | ||||
| 	repo, err := repo_model.GetRepositoryByID(ctx, repoID) | ||||
| 	if err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
|  | ||||
| 	log.Debug("SyncRepoBranches: in Repo[%d:%s]", repo.ID, repo.FullName()) | ||||
|  | ||||
| 	gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) | ||||
| 	if err != nil { | ||||
| 		log.Error("OpenRepository[%s]: %w", repo.RepoPath(), err) | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	defer gitRepo.Close() | ||||
|  | ||||
| 	return SyncRepoBranchesWithRepo(ctx, repo, gitRepo, doerID) | ||||
| } | ||||
|  | ||||
| func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doerID int64) (int64, error) { | ||||
| 	allBranches := container.Set[string]{} | ||||
| 	{ | ||||
| 		branches, _, err := gitRepo.GetBranchNames(0, 0) | ||||
| 		if err != nil { | ||||
| 			return 0, err | ||||
| 		} | ||||
| 		log.Trace("SyncRepoBranches[%s]: branches[%d]: %v", repo.FullName(), len(branches), branches) | ||||
| 		for _, branch := range branches { | ||||
| 			allBranches.Add(branch) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	dbBranches := make(map[string]*git_model.Branch) | ||||
| 	{ | ||||
| 		branches, err := git_model.FindBranches(ctx, git_model.FindBranchOptions{ | ||||
| 			ListOptions: db.ListOptions{ | ||||
| 				ListAll: true, | ||||
| 			}, | ||||
| 			RepoID: repo.ID, | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			return 0, err | ||||
| 		} | ||||
| 		for _, branch := range branches { | ||||
| 			dbBranches[branch.Name] = branch | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var toAdd []*git_model.Branch | ||||
| 	var toUpdate []*git_model.Branch | ||||
| 	var toRemove []int64 | ||||
| 	for branch := range allBranches { | ||||
| 		dbb := dbBranches[branch] | ||||
| 		commit, err := gitRepo.GetBranchCommit(branch) | ||||
| 		if err != nil { | ||||
| 			return 0, err | ||||
| 		} | ||||
| 		if dbb == nil { | ||||
| 			toAdd = append(toAdd, &git_model.Branch{ | ||||
| 				RepoID:        repo.ID, | ||||
| 				Name:          branch, | ||||
| 				CommitID:      commit.ID.String(), | ||||
| 				CommitMessage: commit.CommitMessage, | ||||
| 				PusherID:      doerID, | ||||
| 				CommitTime:    timeutil.TimeStamp(commit.Author.When.Unix()), | ||||
| 			}) | ||||
| 		} else if commit.ID.String() != dbb.CommitID { | ||||
| 			toUpdate = append(toUpdate, &git_model.Branch{ | ||||
| 				ID:            dbb.ID, | ||||
| 				RepoID:        repo.ID, | ||||
| 				Name:          branch, | ||||
| 				CommitID:      commit.ID.String(), | ||||
| 				CommitMessage: commit.CommitMessage, | ||||
| 				PusherID:      doerID, | ||||
| 				CommitTime:    timeutil.TimeStamp(commit.Author.When.Unix()), | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for _, dbBranch := range dbBranches { | ||||
| 		if !allBranches.Contains(dbBranch.Name) && !dbBranch.IsDeleted { | ||||
| 			toRemove = append(toRemove, dbBranch.ID) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	log.Trace("SyncRepoBranches[%s]: toAdd: %v, toUpdate: %v, toRemove: %v", repo.FullName(), toAdd, toUpdate, toRemove) | ||||
|  | ||||
| 	if len(toAdd) == 0 && len(toRemove) == 0 && len(toUpdate) == 0 { | ||||
| 		return int64(len(allBranches)), nil | ||||
| 	} | ||||
|  | ||||
| 	if err := db.WithTx(ctx, func(subCtx context.Context) error { | ||||
| 		if len(toAdd) > 0 { | ||||
| 			if err := git_model.AddBranches(subCtx, toAdd); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		for _, b := range toUpdate { | ||||
| 			if _, err := db.GetEngine(subCtx).ID(b.ID). | ||||
| 				Cols("commit_id, commit_message, pusher_id, commit_time, is_deleted"). | ||||
| 				Update(b); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if len(toRemove) > 0 { | ||||
| 			if err := git_model.DeleteBranches(subCtx, repo.ID, doerID, toRemove); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return nil | ||||
| 	}); err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	return int64(len(allBranches)), nil | ||||
| } | ||||
| @@ -351,6 +351,12 @@ func initRepository(ctx context.Context, repoPath string, u *user_model.User, re | ||||
| 		if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { | ||||
| 			return fmt.Errorf("setDefaultBranch: %w", err) | ||||
| 		} | ||||
|  | ||||
| 		if !repo.IsEmpty { | ||||
| 			if _, err := SyncRepoBranches(ctx, repo.ID, u.ID); err != nil { | ||||
| 				return fmt.Errorf("SyncRepoBranches: %w", err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err = UpdateRepository(ctx, repo, false); err != nil { | ||||
|   | ||||
| @@ -151,6 +151,10 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if _, err := SyncRepoBranchesWithRepo(ctx, repo, gitRepo, u.ID); err != nil { | ||||
| 			return repo, fmt.Errorf("SyncRepoBranchesWithRepo: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		if !opts.Releases { | ||||
| 			// note: this will greatly improve release (tag) sync | ||||
| 			// for pull-mirrors with many tags | ||||
| @@ -169,7 +173,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	ctx, committer, err := db.TxContext(db.DefaultContext) | ||||
| 	ctx, committer, err := db.TxContext(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|   | ||||
| @@ -2660,6 +2660,7 @@ dashboard.delete_repo_archives.started = Delete all repository archives task sta | ||||
| dashboard.delete_missing_repos = Delete all repositories missing their Git files | ||||
| dashboard.delete_missing_repos.started = Delete all repositories missing their Git files task started. | ||||
| dashboard.delete_generated_repository_avatars = Delete generated repository avatars | ||||
| dashboard.sync_repo_branches = Sync missed branches from git data to databases | ||||
| dashboard.update_mirrors = Update Mirrors | ||||
| dashboard.repo_health_check = Health check all repositories | ||||
| dashboard.check_repo_stats = Check all repository statistics | ||||
| @@ -2713,6 +2714,7 @@ dashboard.gc_lfs = Garbage collect LFS meta objects | ||||
| dashboard.stop_zombie_tasks = Stop zombie tasks | ||||
| dashboard.stop_endless_tasks = Stop endless tasks | ||||
| dashboard.cancel_abandoned_jobs = Cancel abandoned jobs | ||||
| dashboard.sync_branch.started = Branches Sync started | ||||
|  | ||||
| users.user_manage_panel = User Account Management | ||||
| users.new_account = Create User Account | ||||
|   | ||||
| @@ -15,7 +15,9 @@ import ( | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	repo_module "code.gitea.io/gitea/modules/repository" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| 	"code.gitea.io/gitea/routers/api/v1/utils" | ||||
| 	"code.gitea.io/gitea/services/convert" | ||||
| @@ -76,7 +78,7 @@ func GetBranch(ctx *context.APIContext) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branch, c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) | ||||
| 	br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branch.Name, c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err) | ||||
| 		return | ||||
| @@ -118,6 +120,37 @@ func DeleteBranch(ctx *context.APIContext) { | ||||
|  | ||||
| 	branchName := ctx.Params("*") | ||||
|  | ||||
| 	if ctx.Repo.Repository.IsEmpty { | ||||
| 		ctx.Error(http.StatusForbidden, "", "Git Repository is empty.") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// check whether branches of this repository has been synced | ||||
| 	totalNumOfBranches, err := git_model.CountBranches(ctx, git_model.FindBranchOptions{ | ||||
| 		RepoID:          ctx.Repo.Repository.ID, | ||||
| 		IsDeletedBranch: util.OptionalBoolFalse, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, "CountBranches", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if totalNumOfBranches == 0 { // sync branches immediately because non-empty repository should have at least 1 branch | ||||
| 		_, err = repo_module.SyncRepoBranches(ctx, ctx.Repo.Repository.ID, 0) | ||||
| 		if err != nil { | ||||
| 			ctx.ServerError("SyncRepoBranches", err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if ctx.Repo.Repository.IsArchived { | ||||
| 		ctx.Error(http.StatusForbidden, "IsArchived", fmt.Errorf("can not delete branch of an archived repository")) | ||||
| 		return | ||||
| 	} | ||||
| 	if ctx.Repo.Repository.IsMirror { | ||||
| 		ctx.Error(http.StatusForbidden, "IsMirrored", fmt.Errorf("can not delete branch of an mirror repository")) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil { | ||||
| 		switch { | ||||
| 		case git.IsErrBranchNotExist(err): | ||||
| @@ -203,14 +236,14 @@ func CreateBranch(ctx *context.APIContext) { | ||||
|  | ||||
| 	err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, oldCommit.ID.String(), opt.BranchName) | ||||
| 	if err != nil { | ||||
| 		if models.IsErrBranchDoesNotExist(err) { | ||||
| 		if git_model.IsErrBranchNotExist(err) { | ||||
| 			ctx.Error(http.StatusNotFound, "", "The old branch does not exist") | ||||
| 		} | ||||
| 		if models.IsErrTagAlreadyExists(err) { | ||||
| 			ctx.Error(http.StatusConflict, "", "The branch with the same tag already exists.") | ||||
| 		} else if models.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) { | ||||
| 		} else if git_model.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) { | ||||
| 			ctx.Error(http.StatusConflict, "", "The branch already exists.") | ||||
| 		} else if models.IsErrBranchNameConflict(err) { | ||||
| 		} else if git_model.IsErrBranchNameConflict(err) { | ||||
| 			ctx.Error(http.StatusConflict, "", "The branch with the same name already exists.") | ||||
| 		} else { | ||||
| 			ctx.Error(http.StatusInternalServerError, "CreateNewBranchFromCommit", err) | ||||
| @@ -236,7 +269,7 @@ func CreateBranch(ctx *context.APIContext) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branch, commit, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) | ||||
| 	br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branch.Name, commit, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err) | ||||
| 		return | ||||
| @@ -275,20 +308,38 @@ func ListBranches(ctx *context.APIContext) { | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/BranchList" | ||||
|  | ||||
| 	var totalNumOfBranches int | ||||
| 	var totalNumOfBranches int64 | ||||
| 	var apiBranches []*api.Branch | ||||
|  | ||||
| 	listOptions := utils.GetListOptions(ctx) | ||||
|  | ||||
| 	if !ctx.Repo.Repository.IsEmpty && ctx.Repo.GitRepo != nil { | ||||
| 		branchOpts := git_model.FindBranchOptions{ | ||||
| 			ListOptions:     listOptions, | ||||
| 			RepoID:          ctx.Repo.Repository.ID, | ||||
| 			IsDeletedBranch: util.OptionalBoolFalse, | ||||
| 		} | ||||
| 		var err error | ||||
| 		totalNumOfBranches, err = git_model.CountBranches(ctx, branchOpts) | ||||
| 		if err != nil { | ||||
| 			ctx.Error(http.StatusInternalServerError, "CountBranches", err) | ||||
| 			return | ||||
| 		} | ||||
| 		if totalNumOfBranches == 0 { // sync branches immediately because non-empty repository should have at least 1 branch | ||||
| 			totalNumOfBranches, err = repo_module.SyncRepoBranches(ctx, ctx.Repo.Repository.ID, 0) | ||||
| 			if err != nil { | ||||
| 				ctx.ServerError("SyncRepoBranches", err) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		rules, err := git_model.FindRepoProtectedBranchRules(ctx, ctx.Repo.Repository.ID) | ||||
| 		if err != nil { | ||||
| 			ctx.Error(http.StatusInternalServerError, "FindMatchedProtectedBranchRules", err) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		skip, _ := listOptions.GetStartEnd() | ||||
| 		branches, total, err := ctx.Repo.GitRepo.GetBranches(skip, listOptions.PageSize) | ||||
| 		branches, err := git_model.FindBranches(ctx, branchOpts) | ||||
| 		if err != nil { | ||||
| 			ctx.Error(http.StatusInternalServerError, "GetBranches", err) | ||||
| 			return | ||||
| @@ -296,11 +347,11 @@ func ListBranches(ctx *context.APIContext) { | ||||
|  | ||||
| 		apiBranches = make([]*api.Branch, 0, len(branches)) | ||||
| 		for i := range branches { | ||||
| 			c, err := branches[i].GetCommit() | ||||
| 			c, err := ctx.Repo.GitRepo.GetBranchCommit(branches[i].Name) | ||||
| 			if err != nil { | ||||
| 				// Skip if this branch doesn't exist anymore. | ||||
| 				if git.IsErrNotExist(err) { | ||||
| 					total-- | ||||
| 					totalNumOfBranches-- | ||||
| 					continue | ||||
| 				} | ||||
| 				ctx.Error(http.StatusInternalServerError, "GetCommit", err) | ||||
| @@ -308,19 +359,17 @@ func ListBranches(ctx *context.APIContext) { | ||||
| 			} | ||||
|  | ||||
| 			branchProtection := rules.GetFirstMatched(branches[i].Name) | ||||
| 			apiBranch, err := convert.ToBranch(ctx, ctx.Repo.Repository, branches[i], c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) | ||||
| 			apiBranch, err := convert.ToBranch(ctx, ctx.Repo.Repository, branches[i].Name, c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) | ||||
| 			if err != nil { | ||||
| 				ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err) | ||||
| 				return | ||||
| 			} | ||||
| 			apiBranches = append(apiBranches, apiBranch) | ||||
| 		} | ||||
|  | ||||
| 		totalNumOfBranches = total | ||||
| 	} | ||||
|  | ||||
| 	ctx.SetLinkHeader(totalNumOfBranches, listOptions.PageSize) | ||||
| 	ctx.SetTotalCountHeader(int64(totalNumOfBranches)) | ||||
| 	ctx.SetLinkHeader(int(totalNumOfBranches), listOptions.PageSize) | ||||
| 	ctx.SetTotalCountHeader(totalNumOfBranches) | ||||
| 	ctx.JSON(http.StatusOK, apiBranches) | ||||
| } | ||||
|  | ||||
| @@ -580,7 +629,7 @@ func CreateBranchProtection(ctx *context.APIContext) { | ||||
| 				}() | ||||
| 			} | ||||
| 			// FIXME: since we only need to recheck files protected rules, we could improve this | ||||
| 			matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.GitRepo, ruleName) | ||||
| 			matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.Repository.ID, ruleName) | ||||
| 			if err != nil { | ||||
| 				ctx.Error(http.StatusInternalServerError, "FindAllMatchedBranches", err) | ||||
| 				return | ||||
| @@ -851,7 +900,7 @@ func EditBranchProtection(ctx *context.APIContext) { | ||||
| 			} | ||||
|  | ||||
| 			// FIXME: since we only need to recheck files protected rules, we could improve this | ||||
| 			matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.GitRepo, protectBranch.RuleName) | ||||
| 			matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.Repository.ID, protectBranch.RuleName) | ||||
| 			if err != nil { | ||||
| 				ctx.Error(http.StatusInternalServerError, "FindAllMatchedBranches", err) | ||||
| 				return | ||||
|   | ||||
| @@ -687,12 +687,12 @@ func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) { | ||||
| 		ctx.Error(http.StatusForbidden, "Access", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if models.IsErrBranchAlreadyExists(err) || models.IsErrFilenameInvalid(err) || models.IsErrSHADoesNotMatch(err) || | ||||
| 	if git_model.IsErrBranchAlreadyExists(err) || models.IsErrFilenameInvalid(err) || models.IsErrSHADoesNotMatch(err) || | ||||
| 		models.IsErrFilePathInvalid(err) || models.IsErrRepoFileAlreadyExists(err) { | ||||
| 		ctx.Error(http.StatusUnprocessableEntity, "Invalid", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if models.IsErrBranchDoesNotExist(err) || git.IsErrBranchNotExist(err) { | ||||
| 	if git_model.IsErrBranchNotExist(err) || git.IsErrBranchNotExist(err) { | ||||
| 		ctx.Error(http.StatusNotFound, "BranchDoesNotExist", err) | ||||
| 		return | ||||
| 	} | ||||
| @@ -843,7 +843,7 @@ func DeleteFile(ctx *context.APIContext) { | ||||
| 		if git.IsErrBranchNotExist(err) || models.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) { | ||||
| 			ctx.Error(http.StatusNotFound, "DeleteFile", err) | ||||
| 			return | ||||
| 		} else if models.IsErrBranchAlreadyExists(err) || | ||||
| 		} else if git_model.IsErrBranchAlreadyExists(err) || | ||||
| 			models.IsErrFilenameInvalid(err) || | ||||
| 			models.IsErrSHADoesNotMatch(err) || | ||||
| 			models.IsErrCommitIDDoesNotMatch(err) || | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	git_model "code.gitea.io/gitea/models/git" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| @@ -91,12 +92,12 @@ func ApplyDiffPatch(ctx *context.APIContext) { | ||||
| 			ctx.Error(http.StatusForbidden, "Access", err) | ||||
| 			return | ||||
| 		} | ||||
| 		if models.IsErrBranchAlreadyExists(err) || models.IsErrFilenameInvalid(err) || models.IsErrSHADoesNotMatch(err) || | ||||
| 		if git_model.IsErrBranchAlreadyExists(err) || models.IsErrFilenameInvalid(err) || models.IsErrSHADoesNotMatch(err) || | ||||
| 			models.IsErrFilePathInvalid(err) || models.IsErrRepoFileAlreadyExists(err) { | ||||
| 			ctx.Error(http.StatusUnprocessableEntity, "Invalid", err) | ||||
| 			return | ||||
| 		} | ||||
| 		if models.IsErrBranchDoesNotExist(err) || git.IsErrBranchNotExist(err) { | ||||
| 		if git_model.IsErrBranchNotExist(err) || git.IsErrBranchNotExist(err) { | ||||
| 			ctx.Error(http.StatusNotFound, "BranchDoesNotExist", err) | ||||
| 			return | ||||
| 		} | ||||
|   | ||||
| @@ -14,12 +14,15 @@ import ( | ||||
| 	activities_model "code.gitea.io/gitea/models/activities" | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/graceful" | ||||
| 	"code.gitea.io/gitea/modules/json" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/updatechecker" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| 	"code.gitea.io/gitea/services/cron" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	repo_service "code.gitea.io/gitea/services/repository" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| @@ -133,12 +136,22 @@ func DashboardPost(ctx *context.Context) { | ||||
|  | ||||
| 	// Run operation. | ||||
| 	if form.Op != "" { | ||||
| 		task := cron.GetTask(form.Op) | ||||
| 		if task != nil { | ||||
| 			go task.RunWithUser(ctx.Doer, nil) | ||||
| 			ctx.Flash.Success(ctx.Tr("admin.dashboard.task.started", ctx.Tr("admin.dashboard."+form.Op))) | ||||
| 		} else { | ||||
| 			ctx.Flash.Error(ctx.Tr("admin.dashboard.task.unknown", form.Op)) | ||||
| 		switch form.Op { | ||||
| 		case "sync_repo_branches": | ||||
| 			go func() { | ||||
| 				if err := repo_service.AddAllRepoBranchesToSyncQueue(graceful.GetManager().ShutdownContext(), ctx.Doer.ID); err != nil { | ||||
| 					log.Error("AddAllRepoBranchesToSyncQueue: %v: %v", ctx.Doer.ID, err) | ||||
| 				} | ||||
| 			}() | ||||
| 			ctx.Flash.Success(ctx.Tr("admin.dashboard.sync_branch.started")) | ||||
| 		default: | ||||
| 			task := cron.GetTask(form.Op) | ||||
| 			if task != nil { | ||||
| 				go task.RunWithUser(ctx.Doer, nil) | ||||
| 				ctx.Flash.Success(ctx.Tr("admin.dashboard.task.started", ctx.Tr("admin.dashboard."+form.Op))) | ||||
| 			} else { | ||||
| 				ctx.Flash.Error(ctx.Tr("admin.dashboard.task.unknown", form.Op)) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if form.From == "monitor" { | ||||
|   | ||||
| @@ -13,7 +13,6 @@ import ( | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	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/unit" | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| @@ -28,32 +27,16 @@ import ( | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	release_service "code.gitea.io/gitea/services/release" | ||||
| 	repo_service "code.gitea.io/gitea/services/repository" | ||||
| 	files_service "code.gitea.io/gitea/services/repository/files" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	tplBranch base.TplName = "repo/branch/list" | ||||
| ) | ||||
|  | ||||
| // Branch contains the branch information | ||||
| type Branch struct { | ||||
| 	Name              string | ||||
| 	Commit            *git.Commit | ||||
| 	IsProtected       bool | ||||
| 	IsDeleted         bool | ||||
| 	IsIncluded        bool | ||||
| 	DeletedBranch     *git_model.DeletedBranch | ||||
| 	CommitsAhead      int | ||||
| 	CommitsBehind     int | ||||
| 	LatestPullRequest *issues_model.PullRequest | ||||
| 	MergeMovedOn      bool | ||||
| } | ||||
|  | ||||
| // Branches render repository branch page | ||||
| func Branches(ctx *context.Context) { | ||||
| 	ctx.Data["Title"] = "Branches" | ||||
| 	ctx.Data["IsRepoToolbarBranches"] = true | ||||
| 	ctx.Data["DefaultBranch"] = ctx.Repo.Repository.DefaultBranch | ||||
| 	ctx.Data["AllowsPulls"] = ctx.Repo.Repository.AllowsPulls() | ||||
| 	ctx.Data["IsWriter"] = ctx.Repo.CanWrite(unit.TypeCode) | ||||
| 	ctx.Data["IsMirror"] = ctx.Repo.Repository.IsMirror | ||||
| @@ -68,15 +51,15 @@ func Branches(ctx *context.Context) { | ||||
| 	} | ||||
| 	pageSize := setting.Git.BranchesRangeSize | ||||
|  | ||||
| 	skip := (page - 1) * pageSize | ||||
| 	log.Debug("Branches: skip: %d limit: %d", skip, pageSize) | ||||
| 	defaultBranchBranch, branches, branchesCount := loadBranches(ctx, skip, pageSize) | ||||
| 	if ctx.Written() { | ||||
| 	defaultBranch, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, util.OptionalBoolNone, page, pageSize) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("LoadBranches", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.Data["Branches"] = branches | ||||
| 	ctx.Data["DefaultBranchBranch"] = defaultBranchBranch | ||||
| 	pager := context.NewPagination(branchesCount, pageSize, page, 5) | ||||
| 	ctx.Data["DefaultBranchBranch"] = defaultBranch | ||||
| 	pager := context.NewPagination(int(branchesCount), pageSize, page, 5) | ||||
| 	pager.SetDefaultParams(ctx) | ||||
| 	ctx.Data["Page"] = pager | ||||
|  | ||||
| @@ -130,7 +113,7 @@ func RestoreBranchPost(ctx *context.Context) { | ||||
|  | ||||
| 	if err := git.Push(ctx, ctx.Repo.Repository.RepoPath(), git.PushOptions{ | ||||
| 		Remote: ctx.Repo.Repository.RepoPath(), | ||||
| 		Branch: fmt.Sprintf("%s:%s%s", deletedBranch.Commit, git.BranchPrefix, deletedBranch.Name), | ||||
| 		Branch: fmt.Sprintf("%s:%s%s", deletedBranch.CommitID, git.BranchPrefix, deletedBranch.Name), | ||||
| 		Env:    repo_module.PushingEnvironment(ctx.Doer, ctx.Repo.Repository), | ||||
| 	}); err != nil { | ||||
| 		if strings.Contains(err.Error(), "already exists") { | ||||
| @@ -148,7 +131,7 @@ func RestoreBranchPost(ctx *context.Context) { | ||||
| 		&repo_module.PushUpdateOptions{ | ||||
| 			RefFullName:  git.RefNameFromBranch(deletedBranch.Name), | ||||
| 			OldCommitID:  git.EmptySHA, | ||||
| 			NewCommitID:  deletedBranch.Commit, | ||||
| 			NewCommitID:  deletedBranch.CommitID, | ||||
| 			PusherID:     ctx.Doer.ID, | ||||
| 			PusherName:   ctx.Doer.Name, | ||||
| 			RepoUserName: ctx.Repo.Owner.Name, | ||||
| @@ -166,180 +149,6 @@ func redirect(ctx *context.Context) { | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // loadBranches loads branches from the repository limited by page & pageSize. | ||||
| // NOTE: May write to context on error. | ||||
| func loadBranches(ctx *context.Context, skip, limit int) (*Branch, []*Branch, int) { | ||||
| 	defaultBranch, err := ctx.Repo.GitRepo.GetBranch(ctx.Repo.Repository.DefaultBranch) | ||||
| 	if err != nil { | ||||
| 		if !git.IsErrBranchNotExist(err) { | ||||
| 			log.Error("loadBranches: get default branch: %v", err) | ||||
| 			ctx.ServerError("GetDefaultBranch", err) | ||||
| 			return nil, nil, 0 | ||||
| 		} | ||||
| 		log.Warn("loadBranches: missing default branch %s for %-v", ctx.Repo.Repository.DefaultBranch, ctx.Repo.Repository) | ||||
| 	} | ||||
|  | ||||
| 	rawBranches, totalNumOfBranches, err := ctx.Repo.GitRepo.GetBranches(skip, limit) | ||||
| 	if err != nil { | ||||
| 		log.Error("GetBranches: %v", err) | ||||
| 		ctx.ServerError("GetBranches", err) | ||||
| 		return nil, nil, 0 | ||||
| 	} | ||||
|  | ||||
| 	rules, err := git_model.FindRepoProtectedBranchRules(ctx, ctx.Repo.Repository.ID) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("FindRepoProtectedBranchRules", err) | ||||
| 		return nil, nil, 0 | ||||
| 	} | ||||
|  | ||||
| 	repoIDToRepo := map[int64]*repo_model.Repository{} | ||||
| 	repoIDToRepo[ctx.Repo.Repository.ID] = ctx.Repo.Repository | ||||
|  | ||||
| 	repoIDToGitRepo := map[int64]*git.Repository{} | ||||
| 	repoIDToGitRepo[ctx.Repo.Repository.ID] = ctx.Repo.GitRepo | ||||
|  | ||||
| 	var branches []*Branch | ||||
| 	for i := range rawBranches { | ||||
| 		if defaultBranch != nil && rawBranches[i].Name == defaultBranch.Name { | ||||
| 			// Skip default branch | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		branch := loadOneBranch(ctx, rawBranches[i], defaultBranch, &rules, repoIDToRepo, repoIDToGitRepo) | ||||
| 		if branch == nil { | ||||
| 			return nil, nil, 0 | ||||
| 		} | ||||
|  | ||||
| 		branches = append(branches, branch) | ||||
| 	} | ||||
|  | ||||
| 	var defaultBranchBranch *Branch | ||||
| 	if defaultBranch != nil { | ||||
| 		// Always add the default branch | ||||
| 		log.Debug("loadOneBranch: load default: '%s'", defaultBranch.Name) | ||||
| 		defaultBranchBranch = loadOneBranch(ctx, defaultBranch, defaultBranch, &rules, repoIDToRepo, repoIDToGitRepo) | ||||
| 		branches = append(branches, defaultBranchBranch) | ||||
| 	} | ||||
|  | ||||
| 	if ctx.Repo.CanWrite(unit.TypeCode) { | ||||
| 		deletedBranches, err := getDeletedBranches(ctx) | ||||
| 		if err != nil { | ||||
| 			ctx.ServerError("getDeletedBranches", err) | ||||
| 			return nil, nil, 0 | ||||
| 		} | ||||
| 		branches = append(branches, deletedBranches...) | ||||
| 	} | ||||
|  | ||||
| 	return defaultBranchBranch, branches, totalNumOfBranches | ||||
| } | ||||
|  | ||||
| func loadOneBranch(ctx *context.Context, rawBranch, defaultBranch *git.Branch, protectedBranches *git_model.ProtectedBranchRules, | ||||
| 	repoIDToRepo map[int64]*repo_model.Repository, | ||||
| 	repoIDToGitRepo map[int64]*git.Repository, | ||||
| ) *Branch { | ||||
| 	log.Trace("loadOneBranch: '%s'", rawBranch.Name) | ||||
|  | ||||
| 	commit, err := rawBranch.GetCommit() | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("GetCommit", err) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	branchName := rawBranch.Name | ||||
| 	p := protectedBranches.GetFirstMatched(branchName) | ||||
| 	isProtected := p != nil | ||||
|  | ||||
| 	divergence := &git.DivergeObject{ | ||||
| 		Ahead:  -1, | ||||
| 		Behind: -1, | ||||
| 	} | ||||
| 	if defaultBranch != nil { | ||||
| 		divergence, err = files_service.CountDivergingCommits(ctx, ctx.Repo.Repository, git.BranchPrefix+branchName) | ||||
| 		if err != nil { | ||||
| 			log.Error("CountDivergingCommits", err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	pr, err := issues_model.GetLatestPullRequestByHeadInfo(ctx.Repo.Repository.ID, branchName) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("GetLatestPullRequestByHeadInfo", err) | ||||
| 		return nil | ||||
| 	} | ||||
| 	headCommit := commit.ID.String() | ||||
|  | ||||
| 	mergeMovedOn := false | ||||
| 	if pr != nil { | ||||
| 		pr.HeadRepo = ctx.Repo.Repository | ||||
| 		if err := pr.LoadIssue(ctx); err != nil { | ||||
| 			ctx.ServerError("LoadIssue", err) | ||||
| 			return nil | ||||
| 		} | ||||
| 		if repo, ok := repoIDToRepo[pr.BaseRepoID]; ok { | ||||
| 			pr.BaseRepo = repo | ||||
| 		} else if err := pr.LoadBaseRepo(ctx); err != nil { | ||||
| 			ctx.ServerError("LoadBaseRepo", err) | ||||
| 			return nil | ||||
| 		} else { | ||||
| 			repoIDToRepo[pr.BaseRepoID] = pr.BaseRepo | ||||
| 		} | ||||
| 		pr.Issue.Repo = pr.BaseRepo | ||||
|  | ||||
| 		if pr.HasMerged { | ||||
| 			baseGitRepo, ok := repoIDToGitRepo[pr.BaseRepoID] | ||||
| 			if !ok { | ||||
| 				baseGitRepo, err = git.OpenRepository(ctx, pr.BaseRepo.RepoPath()) | ||||
| 				if err != nil { | ||||
| 					ctx.ServerError("OpenRepository", err) | ||||
| 					return nil | ||||
| 				} | ||||
| 				defer baseGitRepo.Close() | ||||
| 				repoIDToGitRepo[pr.BaseRepoID] = baseGitRepo | ||||
| 			} | ||||
| 			pullCommit, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName()) | ||||
| 			if err != nil && !git.IsErrNotExist(err) { | ||||
| 				ctx.ServerError("GetBranchCommitID", err) | ||||
| 				return nil | ||||
| 			} | ||||
| 			if err == nil && headCommit != pullCommit { | ||||
| 				// the head has moved on from the merge - we shouldn't delete | ||||
| 				mergeMovedOn = true | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	isIncluded := divergence.Ahead == 0 && ctx.Repo.Repository.DefaultBranch != branchName | ||||
| 	return &Branch{ | ||||
| 		Name:              branchName, | ||||
| 		Commit:            commit, | ||||
| 		IsProtected:       isProtected, | ||||
| 		IsIncluded:        isIncluded, | ||||
| 		CommitsAhead:      divergence.Ahead, | ||||
| 		CommitsBehind:     divergence.Behind, | ||||
| 		LatestPullRequest: pr, | ||||
| 		MergeMovedOn:      mergeMovedOn, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func getDeletedBranches(ctx *context.Context) ([]*Branch, error) { | ||||
| 	branches := []*Branch{} | ||||
|  | ||||
| 	deletedBranches, err := git_model.GetDeletedBranches(ctx, ctx.Repo.Repository.ID) | ||||
| 	if err != nil { | ||||
| 		return branches, err | ||||
| 	} | ||||
|  | ||||
| 	for i := range deletedBranches { | ||||
| 		deletedBranches[i].LoadUser(ctx) | ||||
| 		branches = append(branches, &Branch{ | ||||
| 			Name:          deletedBranches[i].Name, | ||||
| 			IsDeleted:     true, | ||||
| 			DeletedBranch: deletedBranches[i], | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return branches, nil | ||||
| } | ||||
|  | ||||
| // CreateBranch creates new branch in repository | ||||
| func CreateBranch(ctx *context.Context) { | ||||
| 	form := web.GetForm(ctx).(*forms.NewBranchForm) | ||||
| @@ -380,13 +189,13 @@ func CreateBranch(ctx *context.Context) { | ||||
| 			ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) | ||||
| 			return | ||||
| 		} | ||||
| 		if models.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) { | ||||
| 		if git_model.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) { | ||||
| 			ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.NewBranchName)) | ||||
| 			ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) | ||||
| 			return | ||||
| 		} | ||||
| 		if models.IsErrBranchNameConflict(err) { | ||||
| 			e := err.(models.ErrBranchNameConflict) | ||||
| 		if git_model.IsErrBranchNameConflict(err) { | ||||
| 			e := err.(git_model.ErrBranchNameConflict) | ||||
| 			ctx.Flash.Error(ctx.Tr("repo.branch.branch_name_conflict", form.NewBranchName, e.BranchName)) | ||||
| 			ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) | ||||
| 			return | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import ( | ||||
| 	"strings" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	git_model "code.gitea.io/gitea/models/git" | ||||
| 	"code.gitea.io/gitea/models/unit" | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| @@ -124,9 +125,9 @@ func CherryPickPost(ctx *context.Context) { | ||||
| 	// First lets try the simple plain read-tree -m approach | ||||
| 	opts.Content = sha | ||||
| 	if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, form.Revert, opts); err != nil { | ||||
| 		if models.IsErrBranchAlreadyExists(err) { | ||||
| 		if git_model.IsErrBranchAlreadyExists(err) { | ||||
| 			// User has specified a branch that already exists | ||||
| 			branchErr := err.(models.ErrBranchAlreadyExists) | ||||
| 			branchErr := err.(git_model.ErrBranchAlreadyExists) | ||||
| 			ctx.Data["Err_NewBranchName"] = true | ||||
| 			ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form) | ||||
| 			return | ||||
| @@ -161,9 +162,9 @@ func CherryPickPost(ctx *context.Context) { | ||||
| 		ctx.Data["FileContent"] = opts.Content | ||||
|  | ||||
| 		if _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil { | ||||
| 			if models.IsErrBranchAlreadyExists(err) { | ||||
| 			if git_model.IsErrBranchAlreadyExists(err) { | ||||
| 				// User has specified a branch that already exists | ||||
| 				branchErr := err.(models.ErrBranchAlreadyExists) | ||||
| 				branchErr := err.(git_model.ErrBranchAlreadyExists) | ||||
| 				ctx.Data["Err_NewBranchName"] = true | ||||
| 				ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form) | ||||
| 				return | ||||
|   | ||||
| @@ -16,6 +16,7 @@ import ( | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	git_model "code.gitea.io/gitea/models/git" | ||||
| 	issues_model "code.gitea.io/gitea/models/issues" | ||||
| 	access_model "code.gitea.io/gitea/models/perm/access" | ||||
| @@ -683,7 +684,13 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor | ||||
| 	} | ||||
| 	defer gitRepo.Close() | ||||
|  | ||||
| 	branches, _, err = gitRepo.GetBranchNames(0, 0) | ||||
| 	branches, err = git_model.FindBranchNames(ctx, git_model.FindBranchOptions{ | ||||
| 		RepoID: repo.ID, | ||||
| 		ListOptions: db.ListOptions{ | ||||
| 			ListAll: true, | ||||
| 		}, | ||||
| 		IsDeletedBranch: util.OptionalBoolFalse, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
| @@ -734,7 +741,13 @@ func CompareDiff(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	headBranches, _, err := ci.HeadGitRepo.GetBranchNames(0, 0) | ||||
| 	headBranches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{ | ||||
| 		RepoID: ci.HeadRepo.ID, | ||||
| 		ListOptions: db.ListOptions{ | ||||
| 			ListAll: true, | ||||
| 		}, | ||||
| 		IsDeletedBranch: util.OptionalBoolFalse, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("GetBranches", err) | ||||
| 		return | ||||
|   | ||||
| @@ -327,10 +327,10 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b | ||||
| 			} else { | ||||
| 				ctx.Error(http.StatusInternalServerError, err.Error()) | ||||
| 			} | ||||
| 		} else if models.IsErrBranchAlreadyExists(err) { | ||||
| 		} else if git_model.IsErrBranchAlreadyExists(err) { | ||||
| 			// For when a user specifies a new branch that already exists | ||||
| 			ctx.Data["Err_NewBranchName"] = true | ||||
| 			if branchErr, ok := err.(models.ErrBranchAlreadyExists); ok { | ||||
| 			if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok { | ||||
| 				ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form) | ||||
| 			} else { | ||||
| 				ctx.Error(http.StatusInternalServerError, err.Error()) | ||||
| @@ -529,9 +529,9 @@ func DeleteFilePost(ctx *context.Context) { | ||||
| 			} else { | ||||
| 				ctx.Error(http.StatusInternalServerError, err.Error()) | ||||
| 			} | ||||
| 		} else if models.IsErrBranchAlreadyExists(err) { | ||||
| 		} else if git_model.IsErrBranchAlreadyExists(err) { | ||||
| 			// For when a user specifies a new branch that already exists | ||||
| 			if branchErr, ok := err.(models.ErrBranchAlreadyExists); ok { | ||||
| 			if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok { | ||||
| 				ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplDeleteFile, &form) | ||||
| 			} else { | ||||
| 				ctx.Error(http.StatusInternalServerError, err.Error()) | ||||
| @@ -731,10 +731,10 @@ func UploadFilePost(ctx *context.Context) { | ||||
| 		} else if git.IsErrBranchNotExist(err) { | ||||
| 			branchErr := err.(git.ErrBranchNotExist) | ||||
| 			ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplUploadFile, &form) | ||||
| 		} else if models.IsErrBranchAlreadyExists(err) { | ||||
| 		} else if git_model.IsErrBranchAlreadyExists(err) { | ||||
| 			// For when a user specifies a new branch that already exists | ||||
| 			ctx.Data["Err_NewBranchName"] = true | ||||
| 			branchErr := err.(models.ErrBranchAlreadyExists) | ||||
| 			branchErr := err.(git_model.ErrBranchAlreadyExists) | ||||
| 			ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplUploadFile, &form) | ||||
| 		} else if git.IsErrPushOutOfDate(err) { | ||||
| 			ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplUploadFile, &form) | ||||
|   | ||||
| @@ -785,7 +785,13 @@ func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	brs, _, err := ctx.Repo.GitRepo.GetBranchNames(0, 0) | ||||
| 	brs, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{ | ||||
| 		RepoID: ctx.Repo.Repository.ID, | ||||
| 		ListOptions: db.ListOptions{ | ||||
| 			ListAll: true, | ||||
| 		}, | ||||
| 		IsDeletedBranch: util.OptionalBoolFalse, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("GetBranches", err) | ||||
| 		return nil | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import ( | ||||
| 	"strings" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	git_model "code.gitea.io/gitea/models/git" | ||||
| 	"code.gitea.io/gitea/models/unit" | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| @@ -94,9 +95,9 @@ func NewDiffPatchPost(ctx *context.Context) { | ||||
| 		Content:      strings.ReplaceAll(form.Content, "\r", ""), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		if models.IsErrBranchAlreadyExists(err) { | ||||
| 		if git_model.IsErrBranchAlreadyExists(err) { | ||||
| 			// User has specified a branch that already exists | ||||
| 			branchErr := err.(models.ErrBranchAlreadyExists) | ||||
| 			branchErr := err.(git_model.ErrBranchAlreadyExists) | ||||
| 			ctx.Data["Err_NewBranchName"] = true | ||||
| 			ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form) | ||||
| 			return | ||||
|   | ||||
| @@ -1493,7 +1493,7 @@ func UpdatePullRequestTarget(ctx *context.Context) { | ||||
| 				"error":      err.Error(), | ||||
| 				"user_error": errorMessage, | ||||
| 			}) | ||||
| 		} else if models.IsErrBranchesEqual(err) { | ||||
| 		} else if git_model.IsErrBranchesEqual(err) { | ||||
| 			errorMessage := ctx.Tr("repo.pulls.nothing_to_compare") | ||||
|  | ||||
| 			ctx.Flash.Error(errorMessage) | ||||
|   | ||||
| @@ -286,7 +286,7 @@ func SettingsProtectedBranchPost(ctx *context.Context) { | ||||
| 	} | ||||
|  | ||||
| 	// FIXME: since we only need to recheck files protected rules, we could improve this | ||||
| 	matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.GitRepo, protectBranch.RuleName) | ||||
| 	matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.Repository.ID, protectBranch.RuleName) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("FindAllMatchedBranches", err) | ||||
| 		return | ||||
|   | ||||
| @@ -50,7 +50,7 @@ func ToEmailSearch(email *user_model.SearchEmailResult) *api.Email { | ||||
| } | ||||
|  | ||||
| // ToBranch convert a git.Commit and git.Branch to an api.Branch | ||||
| func ToBranch(ctx context.Context, repo *repo_model.Repository, b *git.Branch, c *git.Commit, bp *git_model.ProtectedBranch, user *user_model.User, isRepoAdmin bool) (*api.Branch, error) { | ||||
| func ToBranch(ctx context.Context, repo *repo_model.Repository, branchName string, c *git.Commit, bp *git_model.ProtectedBranch, user *user_model.User, isRepoAdmin bool) (*api.Branch, error) { | ||||
| 	if bp == nil { | ||||
| 		var hasPerm bool | ||||
| 		var canPush bool | ||||
| @@ -65,11 +65,11 @@ func ToBranch(ctx context.Context, repo *repo_model.Repository, b *git.Branch, c | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			canPush = issues_model.CanMaintainerWriteToBranch(perms, b.Name, user) | ||||
| 			canPush = issues_model.CanMaintainerWriteToBranch(perms, branchName, user) | ||||
| 		} | ||||
|  | ||||
| 		return &api.Branch{ | ||||
| 			Name:                b.Name, | ||||
| 			Name:                branchName, | ||||
| 			Commit:              ToPayloadCommit(ctx, repo, c), | ||||
| 			Protected:           false, | ||||
| 			RequiredApprovals:   0, | ||||
| @@ -81,7 +81,7 @@ func ToBranch(ctx context.Context, repo *repo_model.Repository, b *git.Branch, c | ||||
| 	} | ||||
|  | ||||
| 	branch := &api.Branch{ | ||||
| 		Name:                b.Name, | ||||
| 		Name:                branchName, | ||||
| 		Commit:              ToPayloadCommit(ctx, repo, c), | ||||
| 		Protected:           true, | ||||
| 		RequiredApprovals:   bp.RequiredApprovals, | ||||
|   | ||||
| @@ -642,7 +642,7 @@ func (g *RepositoryDumper) Finish() error { | ||||
|  | ||||
| // DumpRepository dump repository according MigrateOptions to a local directory | ||||
| func DumpRepository(ctx context.Context, baseDir, ownerName string, opts base.MigrateOptions) error { | ||||
| 	doer, err := user_model.GetAdminUser() | ||||
| 	doer, err := user_model.GetAdminUser(ctx) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @@ -705,7 +705,7 @@ func updateOptionsUnits(opts *base.MigrateOptions, units []string) error { | ||||
|  | ||||
| // RestoreRepository restore a repository from the disk directory | ||||
| func RestoreRepository(ctx context.Context, baseDir, ownerName, repoName string, units []string, validation bool) error { | ||||
| 	doer, err := user_model.GetAdminUser() | ||||
| 	doer, err := user_model.GetAdminUser(ctx) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|   | ||||
| @@ -170,7 +170,7 @@ func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer | ||||
| 		return err | ||||
| 	} | ||||
| 	if branchesEqual { | ||||
| 		return models.ErrBranchesEqual{ | ||||
| 		return git_model.ErrBranchesEqual{ | ||||
| 			HeadBranchName: pr.HeadBranch, | ||||
| 			BaseBranchName: targetBranch, | ||||
| 		} | ||||
| @@ -338,7 +338,7 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, | ||||
| 		for _, pr := range prs { | ||||
| 			divergence, err := GetDiverging(ctx, pr) | ||||
| 			if err != nil { | ||||
| 				if models.IsErrBranchDoesNotExist(err) && !git.IsBranchExist(ctx, pr.HeadRepo.RepoPath(), pr.HeadBranch) { | ||||
| 				if git_model.IsErrBranchNotExist(err) && !git.IsBranchExist(ctx, pr.HeadRepo.RepoPath(), pr.HeadBranch) { | ||||
| 					log.Warn("Cannot test PR %s/%d: head_branch %s no longer exists", pr.BaseRepo.Name, pr.IssueID, pr.HeadBranch) | ||||
| 				} else { | ||||
| 					log.Error("GetDiverging: %v", err) | ||||
|   | ||||
| @@ -11,7 +11,7 @@ import ( | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	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/modules/git" | ||||
| @@ -181,7 +181,7 @@ func createTemporaryRepoForPR(ctx context.Context, pr *issues_model.PullRequest) | ||||
| 		Run(prCtx.RunOpts()); err != nil { | ||||
| 		cancel() | ||||
| 		if !git.IsBranchExist(ctx, pr.HeadRepo.RepoPath(), pr.HeadBranch) { | ||||
| 			return nil, nil, models.ErrBranchDoesNotExist{ | ||||
| 			return nil, nil, git_model.ErrBranchNotExist{ | ||||
| 				BranchName: pr.HeadBranch, | ||||
| 			} | ||||
| 		} | ||||
|   | ||||
| @@ -7,7 +7,6 @@ import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	git_model "code.gitea.io/gitea/models/git" | ||||
| 	issues_model "code.gitea.io/gitea/models/issues" | ||||
| 	access_model "code.gitea.io/gitea/models/perm/access" | ||||
| @@ -168,7 +167,7 @@ func GetDiverging(ctx context.Context, pr *issues_model.PullRequest) (*git.Diver | ||||
| 	log.Trace("GetDiverging[%-v]: compare commits", pr) | ||||
| 	prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr) | ||||
| 	if err != nil { | ||||
| 		if !models.IsErrBranchDoesNotExist(err) { | ||||
| 		if !git_model.IsErrBranchNotExist(err) { | ||||
| 			log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err) | ||||
| 		} | ||||
| 		return nil, err | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import ( | ||||
| 	"strings" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	git_model "code.gitea.io/gitea/models/git" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/container" | ||||
| @@ -146,7 +147,15 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	branches, _, _ := gitRepo.GetBranchNames(0, 0) | ||||
|  | ||||
| 	branches, _ := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{ | ||||
| 		RepoID: repo.ID, | ||||
| 		ListOptions: db.ListOptions{ | ||||
| 			ListAll: true, | ||||
| 		}, | ||||
| 		IsDeletedBranch: util.OptionalBoolFalse, | ||||
| 	}) | ||||
|  | ||||
| 	found := false | ||||
| 	hasDefault := false | ||||
| 	hasMaster := false | ||||
|   | ||||
| @@ -10,13 +10,21 @@ import ( | ||||
| 	"strings" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"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" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/graceful" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/notification" | ||||
| 	"code.gitea.io/gitea/modules/queue" | ||||
| 	repo_module "code.gitea.io/gitea/modules/repository" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	files_service "code.gitea.io/gitea/services/repository/files" | ||||
|  | ||||
| 	"xorm.io/builder" | ||||
| ) | ||||
|  | ||||
| // CreateNewBranch creates a new repository branch | ||||
| @@ -27,7 +35,7 @@ func CreateNewBranch(ctx context.Context, doer *user_model.User, repo *repo_mode | ||||
| 	} | ||||
|  | ||||
| 	if !git.IsBranchExist(ctx, repo.RepoPath(), oldBranchName) { | ||||
| 		return models.ErrBranchDoesNotExist{ | ||||
| 		return git_model.ErrBranchNotExist{ | ||||
| 			BranchName: oldBranchName, | ||||
| 		} | ||||
| 	} | ||||
| @@ -40,16 +48,165 @@ func CreateNewBranch(ctx context.Context, doer *user_model.User, repo *repo_mode | ||||
| 		if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) { | ||||
| 			return err | ||||
| 		} | ||||
| 		return fmt.Errorf("Push: %w", err) | ||||
| 		return fmt.Errorf("push: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // GetBranches returns branches from the repository, skipping skip initial branches and | ||||
| // returning at most limit branches, or all branches if limit is 0. | ||||
| func GetBranches(ctx context.Context, repo *repo_model.Repository, skip, limit int) ([]*git.Branch, int, error) { | ||||
| 	return git.GetBranchesByPath(ctx, repo.RepoPath(), skip, limit) | ||||
| // Branch contains the branch information | ||||
| type Branch struct { | ||||
| 	DBBranch          *git_model.Branch | ||||
| 	IsProtected       bool | ||||
| 	IsIncluded        bool | ||||
| 	CommitsAhead      int | ||||
| 	CommitsBehind     int | ||||
| 	LatestPullRequest *issues_model.PullRequest | ||||
| 	MergeMovedOn      bool | ||||
| } | ||||
|  | ||||
| // LoadBranches loads branches from the repository limited by page & pageSize. | ||||
| func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, isDeletedBranch util.OptionalBool, page, pageSize int) (*Branch, []*Branch, int64, error) { | ||||
| 	defaultDBBranch, err := git_model.GetBranch(ctx, repo.ID, repo.DefaultBranch) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, 0, err | ||||
| 	} | ||||
|  | ||||
| 	branchOpts := git_model.FindBranchOptions{ | ||||
| 		RepoID:          repo.ID, | ||||
| 		IsDeletedBranch: isDeletedBranch, | ||||
| 		ListOptions: db.ListOptions{ | ||||
| 			Page:     page, | ||||
| 			PageSize: pageSize, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	totalNumOfBranches, err := git_model.CountBranches(ctx, branchOpts) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, 0, err | ||||
| 	} | ||||
|  | ||||
| 	branchOpts.ExcludeBranchNames = []string{repo.DefaultBranch} | ||||
|  | ||||
| 	dbBranches, err := git_model.FindBranches(ctx, branchOpts) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, 0, err | ||||
| 	} | ||||
|  | ||||
| 	if err := dbBranches.LoadDeletedBy(ctx); err != nil { | ||||
| 		return nil, nil, 0, err | ||||
| 	} | ||||
| 	if err := dbBranches.LoadPusher(ctx); err != nil { | ||||
| 		return nil, nil, 0, err | ||||
| 	} | ||||
|  | ||||
| 	rules, err := git_model.FindRepoProtectedBranchRules(ctx, repo.ID) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, 0, err | ||||
| 	} | ||||
|  | ||||
| 	repoIDToRepo := map[int64]*repo_model.Repository{} | ||||
| 	repoIDToRepo[repo.ID] = repo | ||||
|  | ||||
| 	repoIDToGitRepo := map[int64]*git.Repository{} | ||||
| 	repoIDToGitRepo[repo.ID] = gitRepo | ||||
|  | ||||
| 	branches := make([]*Branch, 0, len(dbBranches)) | ||||
| 	for i := range dbBranches { | ||||
| 		branch, err := loadOneBranch(ctx, repo, dbBranches[i], &rules, repoIDToRepo, repoIDToGitRepo) | ||||
| 		if err != nil { | ||||
| 			return nil, nil, 0, fmt.Errorf("loadOneBranch: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		branches = append(branches, branch) | ||||
| 	} | ||||
|  | ||||
| 	// Always add the default branch | ||||
| 	log.Debug("loadOneBranch: load default: '%s'", defaultDBBranch.Name) | ||||
| 	defaultBranch, err := loadOneBranch(ctx, repo, defaultDBBranch, &rules, repoIDToRepo, repoIDToGitRepo) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, 0, fmt.Errorf("loadOneBranch: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	return defaultBranch, branches, totalNumOfBranches, nil | ||||
| } | ||||
|  | ||||
| func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *git_model.Branch, protectedBranches *git_model.ProtectedBranchRules, | ||||
| 	repoIDToRepo map[int64]*repo_model.Repository, | ||||
| 	repoIDToGitRepo map[int64]*git.Repository, | ||||
| ) (*Branch, error) { | ||||
| 	log.Trace("loadOneBranch: '%s'", dbBranch.Name) | ||||
|  | ||||
| 	branchName := dbBranch.Name | ||||
| 	p := protectedBranches.GetFirstMatched(branchName) | ||||
| 	isProtected := p != nil | ||||
|  | ||||
| 	divergence := &git.DivergeObject{ | ||||
| 		Ahead:  -1, | ||||
| 		Behind: -1, | ||||
| 	} | ||||
|  | ||||
| 	// it's not default branch | ||||
| 	if repo.DefaultBranch != dbBranch.Name && !dbBranch.IsDeleted { | ||||
| 		var err error | ||||
| 		divergence, err = files_service.CountDivergingCommits(ctx, repo, git.BranchPrefix+branchName) | ||||
| 		if err != nil { | ||||
| 			log.Error("CountDivergingCommits: %v", err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	pr, err := issues_model.GetLatestPullRequestByHeadInfo(repo.ID, branchName) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("GetLatestPullRequestByHeadInfo: %v", err) | ||||
| 	} | ||||
| 	headCommit := dbBranch.CommitID | ||||
|  | ||||
| 	mergeMovedOn := false | ||||
| 	if pr != nil { | ||||
| 		pr.HeadRepo = repo | ||||
| 		if err := pr.LoadIssue(ctx); err != nil { | ||||
| 			return nil, fmt.Errorf("LoadIssue: %v", err) | ||||
| 		} | ||||
| 		if repo, ok := repoIDToRepo[pr.BaseRepoID]; ok { | ||||
| 			pr.BaseRepo = repo | ||||
| 		} else if err := pr.LoadBaseRepo(ctx); err != nil { | ||||
| 			return nil, fmt.Errorf("LoadBaseRepo: %v", err) | ||||
| 		} else { | ||||
| 			repoIDToRepo[pr.BaseRepoID] = pr.BaseRepo | ||||
| 		} | ||||
| 		pr.Issue.Repo = pr.BaseRepo | ||||
|  | ||||
| 		if pr.HasMerged { | ||||
| 			baseGitRepo, ok := repoIDToGitRepo[pr.BaseRepoID] | ||||
| 			if !ok { | ||||
| 				baseGitRepo, err = git.OpenRepository(ctx, pr.BaseRepo.RepoPath()) | ||||
| 				if err != nil { | ||||
| 					return nil, fmt.Errorf("OpenRepository: %v", err) | ||||
| 				} | ||||
| 				defer baseGitRepo.Close() | ||||
| 				repoIDToGitRepo[pr.BaseRepoID] = baseGitRepo | ||||
| 			} | ||||
| 			pullCommit, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName()) | ||||
| 			if err != nil && !git.IsErrNotExist(err) { | ||||
| 				return nil, fmt.Errorf("GetBranchCommitID: %v", err) | ||||
| 			} | ||||
| 			if err == nil && headCommit != pullCommit { | ||||
| 				// the head has moved on from the merge - we shouldn't delete | ||||
| 				mergeMovedOn = true | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	isIncluded := divergence.Ahead == 0 && repo.DefaultBranch != branchName | ||||
| 	return &Branch{ | ||||
| 		DBBranch:          dbBranch, | ||||
| 		IsProtected:       isProtected, | ||||
| 		IsIncluded:        isIncluded, | ||||
| 		CommitsAhead:      divergence.Ahead, | ||||
| 		CommitsBehind:     divergence.Behind, | ||||
| 		LatestPullRequest: pr, | ||||
| 		MergeMovedOn:      mergeMovedOn, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func GetBranchCommitID(ctx context.Context, repo *repo_model.Repository, branch string) (string, error) { | ||||
| @@ -62,17 +219,17 @@ func checkBranchName(ctx context.Context, repo *repo_model.Repository, name stri | ||||
| 		branchRefName := strings.TrimPrefix(refName, git.BranchPrefix) | ||||
| 		switch { | ||||
| 		case branchRefName == name: | ||||
| 			return models.ErrBranchAlreadyExists{ | ||||
| 			return git_model.ErrBranchAlreadyExists{ | ||||
| 				BranchName: name, | ||||
| 			} | ||||
| 		// If branchRefName like a/b but we want to create a branch named a then we have a conflict | ||||
| 		case strings.HasPrefix(branchRefName, name+"/"): | ||||
| 			return models.ErrBranchNameConflict{ | ||||
| 			return git_model.ErrBranchNameConflict{ | ||||
| 				BranchName: branchRefName, | ||||
| 			} | ||||
| 			// Conversely if branchRefName like a but we want to create a branch named a/b then we also have a conflict | ||||
| 		case strings.HasPrefix(name, branchRefName+"/"): | ||||
| 			return models.ErrBranchNameConflict{ | ||||
| 			return git_model.ErrBranchNameConflict{ | ||||
| 				BranchName: branchRefName, | ||||
| 			} | ||||
| 		case refName == git.TagPrefix+name: | ||||
| @@ -101,7 +258,7 @@ func CreateNewBranchFromCommit(ctx context.Context, doer *user_model.User, repo | ||||
| 		if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) { | ||||
| 			return err | ||||
| 		} | ||||
| 		return fmt.Errorf("Push: %w", err) | ||||
| 		return fmt.Errorf("push: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| @@ -169,13 +326,28 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R | ||||
| 		return git_model.ErrBranchIsProtected | ||||
| 	} | ||||
|  | ||||
| 	rawBranch, err := git_model.GetBranch(ctx, repo.ID, branchName) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("GetBranch: %vc", err) | ||||
| 	} | ||||
|  | ||||
| 	if rawBranch.IsDeleted { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	commit, err := gitRepo.GetBranchCommit(branchName) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if err := gitRepo.DeleteBranch(branchName, git.DeleteBranchOptions{ | ||||
| 		Force: true, | ||||
| 	if err := db.WithTx(ctx, func(ctx context.Context) error { | ||||
| 		if err := git_model.AddDeletedBranch(ctx, repo.ID, branchName, doer.ID); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		return gitRepo.DeleteBranch(branchName, git.DeleteBranchOptions{ | ||||
| 			Force: true, | ||||
| 		}) | ||||
| 	}); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @@ -196,3 +368,45 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| type BranchSyncOptions struct { | ||||
| 	RepoID int64 | ||||
| } | ||||
|  | ||||
| // branchSyncQueue represents a queue to handle branch sync jobs. | ||||
| var branchSyncQueue *queue.WorkerPoolQueue[*BranchSyncOptions] | ||||
|  | ||||
| func handlerBranchSync(items ...*BranchSyncOptions) []*BranchSyncOptions { | ||||
| 	for _, opts := range items { | ||||
| 		_, err := repo_module.SyncRepoBranches(graceful.GetManager().ShutdownContext(), opts.RepoID, 0) | ||||
| 		if err != nil { | ||||
| 			log.Error("syncRepoBranches [%d] failed: %v", opts.RepoID, err) | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func addRepoToBranchSyncQueue(repoID, doerID int64) error { | ||||
| 	return branchSyncQueue.Push(&BranchSyncOptions{ | ||||
| 		RepoID: repoID, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func initBranchSyncQueue(ctx context.Context) error { | ||||
| 	branchSyncQueue = queue.CreateUniqueQueue(ctx, "branch_sync", handlerBranchSync) | ||||
| 	if branchSyncQueue == nil { | ||||
| 		return errors.New("unable to create branch_sync queue") | ||||
| 	} | ||||
| 	go graceful.GetManager().RunWithCancel(branchSyncQueue) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func AddAllRepoBranchesToSyncQueue(ctx context.Context, doerID int64) error { | ||||
| 	if err := db.Iterate(ctx, builder.Eq{"is_empty": false}, func(ctx context.Context, repo *repo_model.Repository) error { | ||||
| 		return addRepoToBranchSyncQueue(repo.ID, doerID) | ||||
| 	}); err != nil { | ||||
| 		return fmt.Errorf("run sync all branches failed: %v", err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|   | ||||
| @@ -58,7 +58,7 @@ func (opts *ApplyDiffPatchOptions) Validate(ctx context.Context, repo *repo_mode | ||||
| 	if opts.NewBranch != opts.OldBranch { | ||||
| 		existingBranch, err := gitRepo.GetBranch(opts.NewBranch) | ||||
| 		if existingBranch != nil { | ||||
| 			return models.ErrBranchAlreadyExists{ | ||||
| 			return git_model.ErrBranchAlreadyExists{ | ||||
| 				BranchName: opts.NewBranch, | ||||
| 			} | ||||
| 		} | ||||
|   | ||||
| @@ -197,7 +197,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use | ||||
| 	if opts.NewBranch != opts.OldBranch { | ||||
| 		existingBranch, err := gitRepo.GetBranch(opts.NewBranch) | ||||
| 		if existingBranch != nil { | ||||
| 			return nil, models.ErrBranchAlreadyExists{ | ||||
| 			return nil, git_model.ErrBranchAlreadyExists{ | ||||
| 				BranchName: opts.NewBranch, | ||||
| 			} | ||||
| 		} | ||||
|   | ||||
| @@ -157,7 +157,15 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork | ||||
| 		if err = repo_module.CreateDelegateHooks(repoPath); err != nil { | ||||
| 			return fmt.Errorf("createDelegateHooks: %w", err) | ||||
| 		} | ||||
| 		return nil | ||||
|  | ||||
| 		gitRepo, err := git.OpenRepository(txCtx, repo.RepoPath()) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("OpenRepository: %w", err) | ||||
| 		} | ||||
| 		defer gitRepo.Close() | ||||
|  | ||||
| 		_, err = repo_module.SyncRepoBranchesWithRepo(txCtx, repo, gitRepo, doer.ID) | ||||
| 		return err | ||||
| 	}) | ||||
| 	needsRollbackInPanic = false | ||||
| 	if err != nil { | ||||
|   | ||||
| @@ -93,7 +93,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { | ||||
| 	defer gitRepo.Close() | ||||
|  | ||||
| 	if err = repo_module.UpdateRepoSize(ctx, repo); err != nil { | ||||
| 		log.Error("Failed to update size for repository: %v", err) | ||||
| 		return fmt.Errorf("Failed to update size for repository: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	addTags := make([]string, 0, len(optsList)) | ||||
| @@ -259,8 +259,8 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { | ||||
|  | ||||
| 				notification.NotifyPushCommits(ctx, pusher, repo, opts, commits) | ||||
|  | ||||
| 				if err = git_model.RemoveDeletedBranchByName(ctx, repo.ID, branch); err != nil { | ||||
| 					log.Error("models.RemoveDeletedBranch %s/%s failed: %v", repo.ID, branch, err) | ||||
| 				if err = git_model.UpdateBranch(ctx, repo.ID, branch, newCommit.ID.String(), newCommit.CommitMessage, opts.PusherID, newCommit.Committer.When); err != nil { | ||||
| 					return fmt.Errorf("git_model.UpdateBranch %s:%s failed: %v", repo.FullName(), branch, err) | ||||
| 				} | ||||
|  | ||||
| 				// Cache for big repository | ||||
| @@ -273,8 +273,9 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { | ||||
| 					// close all related pulls | ||||
| 					log.Error("close related pull request failed: %v", err) | ||||
| 				} | ||||
| 				if err := git_model.AddDeletedBranch(db.DefaultContext, repo.ID, branch, opts.OldCommitID, pusher.ID); err != nil { | ||||
| 					log.Warn("AddDeletedBranch: %v", err) | ||||
|  | ||||
| 				if err := git_model.AddDeletedBranch(db.DefaultContext, repo.ID, branch, pusher.ID); err != nil { | ||||
| 					return fmt.Errorf("AddDeletedBranch %s:%s failed: %v", repo.FullName(), branch, err) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
|   | ||||
| @@ -17,6 +17,7 @@ import ( | ||||
| 	system_model "code.gitea.io/gitea/models/system" | ||||
| 	"code.gitea.io/gitea/models/unit" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/graceful" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/notification" | ||||
| 	repo_module "code.gitea.io/gitea/modules/repository" | ||||
| @@ -100,7 +101,10 @@ func Init() error { | ||||
| 	} | ||||
| 	system_model.RemoveAllWithNotice(db.DefaultContext, "Clean up temporary repository uploads", setting.Repository.Upload.TempPath) | ||||
| 	system_model.RemoveAllWithNotice(db.DefaultContext, "Clean up temporary repositories", repo_module.LocalCopyPath()) | ||||
| 	return initPushQueue() | ||||
| 	if err := initPushQueue(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return initBranchSyncQueue(graceful.GetManager().ShutdownContext()) | ||||
| } | ||||
|  | ||||
| // UpdateRepository updates a repository | ||||
|   | ||||
| @@ -61,6 +61,10 @@ | ||||
| 							<td>{{.locale.Tr "admin.dashboard.delete_generated_repository_avatars"}}</td> | ||||
| 							<td class="text right"><button type="submit" class="ui green button" name="op" value="delete_generated_repository_avatars">{{svg "octicon-play"}} {{.locale.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 						</tr> | ||||
| 						<tr> | ||||
| 							<td>{{.locale.Tr "admin.dashboard.sync_repo_branches"}}</td> | ||||
| 							<td class="text right"><button type="submit" class="ui green button" name="op" value="sync_repo_branches">{{svg "octicon-play"}} {{.locale.Tr "admin.dashboard.operation_run"}}</button></td> | ||||
| 						</tr> | ||||
| 					</tbody> | ||||
| 				</table> | ||||
| 			</form> | ||||
|   | ||||
| @@ -22,29 +22,29 @@ | ||||
| 								{{if .DefaultBranchBranch.IsProtected}} | ||||
| 									{{svg "octicon-shield-lock"}} | ||||
| 								{{end}} | ||||
| 								<a href="{{.RepoLink}}/src/branch/{{PathEscapeSegments .DefaultBranch}}">{{.DefaultBranch}}</a> | ||||
| 								<p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.Commit.ID.String}}">{{ShortSha .DefaultBranchBranch.Commit.ID.String}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DefaultBranchBranch.Commit.CommitMessage .RepoLink .Repository.ComposeMetas}}</span> · {{.locale.Tr "org.repo_updated"}} {{TimeSince .DefaultBranchBranch.Commit.Committer.When .locale}}</p> | ||||
| 								<a href="{{.RepoLink}}/src/branch/{{PathEscapeSegments .DefaultBranchBranch.DBBranch.Name}}">{{.DefaultBranchBranch.DBBranch.Name}}</a> | ||||
| 								<p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DefaultBranchBranch.DBBranch.CommitMessage .RepoLink .Repository.ComposeMetas}}</span> · {{.locale.Tr "org.repo_updated"}} {{TimeSince .DefaultBranchBranch.DBBranch.CommitTime.AsTime .locale}}{{if .DefaultBranchBranch.DBBranch.Pusher}}  {{template "shared/user/avatarlink" dict "Context" $.Context "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p> | ||||
| 							</td> | ||||
| 							<td class="right aligned overflow-visible"> | ||||
| 								{{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted)}} | ||||
| 									<button class="btn interact-bg show-create-branch-modal gt-p-3" | ||||
| 										data-modal="#create-branch-modal" | ||||
| 										data-branch-from="{{$.DefaultBranch}}" | ||||
| 										data-branch-from-urlcomponent="{{PathEscapeSegments $.DefaultBranch}}" | ||||
| 										data-tooltip-content="{{$.locale.Tr "repo.branch.new_branch_from" ($.DefaultBranch)}}" | ||||
| 										data-branch-from="{{$.DefaultBranchBranch}}" | ||||
| 										data-branch-from-urlcomponent="{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}" | ||||
| 										data-tooltip-content="{{$.locale.Tr "repo.branch.new_branch_from" ($.DefaultBranchBranch.DBBranch.Name)}}" | ||||
| 									> | ||||
| 										{{svg "octicon-git-branch"}} | ||||
| 									</button> | ||||
| 								{{end}} | ||||
| 								{{if .EnableFeed}} | ||||
| 									<a role="button" class="btn interact-bg gt-p-3" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .DefaultBranch}}">{{svg "octicon-rss"}}</a> | ||||
| 									<a role="button" class="btn interact-bg gt-p-3" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .DefaultBranchBranch.DBBranch.Name}}">{{svg "octicon-rss"}}</a> | ||||
| 								{{end}} | ||||
| 								{{if not $.DisableDownloadSourceArchives}} | ||||
| 									<div class="ui dropdown btn interact-bg gt-p-3" data-tooltip-content="{{$.locale.Tr "repo.branch.download" ($.DefaultBranch)}}"> | ||||
| 									<div class="ui dropdown btn interact-bg gt-p-3" data-tooltip-content="{{$.locale.Tr "repo.branch.download" ($.DefaultBranchBranch.DBBranch.Name)}}"> | ||||
| 										{{svg "octicon-download"}} | ||||
| 										<div class="menu"> | ||||
| 											<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.DefaultBranch}}.zip" rel="nofollow">{{svg "octicon-file-zip"}} ZIP</a> | ||||
| 											<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.DefaultBranch}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}} TAR.GZ</a> | ||||
| 											<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}.zip" rel="nofollow">{{svg "octicon-file-zip"}} ZIP</a> | ||||
| 											<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}} TAR.GZ</a> | ||||
| 										</div> | ||||
| 									</div> | ||||
| 								{{end}} | ||||
| @@ -52,8 +52,8 @@ | ||||
| 									<button class="btn interact-bg gt-p-3 show-modal show-rename-branch-modal" | ||||
| 										data-is-default-branch="true" | ||||
| 										data-modal="#rename-branch-modal" | ||||
| 										data-old-branch-name="{{$.DefaultBranch}}" | ||||
| 										data-tooltip-content="{{$.locale.Tr "repo.branch.rename" ($.DefaultBranch)}}" | ||||
| 										data-old-branch-name="{{$.DefaultBranchBranch}}" | ||||
| 										data-tooltip-content="{{$.locale.Tr "repo.branch.rename" ($.DefaultBranchBranch.DBBranch.Name)}}" | ||||
| 									> | ||||
| 										{{svg "octicon-pencil"}} | ||||
| 									</button> | ||||
| @@ -65,7 +65,7 @@ | ||||
| 			</div> | ||||
| 		{{end}} | ||||
|  | ||||
| 		{{if gt (len .Branches) 1}} | ||||
| 		{{if .Branches}} | ||||
| 			<h4 class="ui top attached header"> | ||||
| 				{{.locale.Tr "repo.branches"}} | ||||
| 			</h4> | ||||
| @@ -73,112 +73,110 @@ | ||||
| 				<table class="ui very basic striped fixed table single line"> | ||||
| 					<tbody> | ||||
| 						{{range .Branches}} | ||||
| 							{{if ne .Name $.DefaultBranch}} | ||||
| 								<tr> | ||||
| 									<td class="six wide"> | ||||
| 									{{if .IsDeleted}} | ||||
| 										<s><a href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .Name}}">{{.Name}}</a></s> | ||||
| 										<p class="info">{{$.locale.Tr "repo.branch.deleted_by" .DeletedBranch.DeletedBy.Name}} {{TimeSinceUnix .DeletedBranch.DeletedUnix $.locale}}</p> | ||||
| 									{{else}} | ||||
| 										{{if .IsProtected}} | ||||
| 											{{svg "octicon-shield-lock"}} | ||||
| 										{{end}} | ||||
| 										<a href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .Name}}">{{.Name}}</a> | ||||
| 										<p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .Commit.ID.String}}">{{ShortSha .Commit.ID.String}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .Commit.CommitMessage $.RepoLink $.Repository.ComposeMetas}}</span> · {{$.locale.Tr "org.repo_updated"}} {{TimeSince .Commit.Committer.When $.locale}}</p> | ||||
| 							<tr> | ||||
| 								<td class="eight wide"> | ||||
| 								{{if .DBBranch.IsDeleted}} | ||||
| 									<s><a href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a></s> | ||||
| 									<p class="info">{{$.locale.Tr "repo.branch.deleted_by" .DBBranch.DeletedBy.Name}} {{TimeSinceUnix .DBBranch.DeletedUnix $.locale}}</p> | ||||
| 								{{else}} | ||||
| 									{{if .IsProtected}} | ||||
| 										{{svg "octicon-shield-lock"}} | ||||
| 									{{end}} | ||||
| 									</td> | ||||
| 									<td class="three wide ui"> | ||||
| 										{{if and (not .IsDeleted) $.DefaultBranchBranch}} | ||||
| 										<div class="commit-divergence"> | ||||
| 											<div class="bar-group"> | ||||
| 												<div class="count count-behind">{{.CommitsBehind}}</div> | ||||
| 												{{/* old code bears 0/0.0 = NaN output, so it might output invalid "width: NaNpx", it just works and doesn't caues any problem. */}} | ||||
| 												<div class="bar bar-behind" style="width: {{Eval 100 "*" .CommitsBehind "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div> | ||||
| 											</div> | ||||
| 											<div class="bar-group"> | ||||
| 												<div class="count count-ahead">{{.CommitsAhead}}</div> | ||||
| 												<div class="bar bar-ahead" style="width: {{Eval 100 "*" .CommitsAhead "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div> | ||||
| 									<a href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a> | ||||
| 									<p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DBBranch.CommitMessage $.RepoLink $.Repository.ComposeMetas}}</span> · {{$.locale.Tr "org.repo_updated"}} {{TimeSince .DBBranch.CommitTime.AsTime $.locale}}{{if .DBBranch.Pusher}}  {{template "shared/user/avatarlink" dict "Context" $.Context "user" .DBBranch.Pusher}}  {{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p> | ||||
| 								{{end}} | ||||
| 								</td> | ||||
| 								<td class="two wide ui"> | ||||
| 									{{if and (not .DBBranch.IsDeleted) $.DefaultBranchBranch}} | ||||
| 									<div class="commit-divergence"> | ||||
| 										<div class="bar-group"> | ||||
| 											<div class="count count-behind">{{.CommitsBehind}}</div> | ||||
| 											{{/* old code bears 0/0.0 = NaN output, so it might output invalid "width: NaNpx", it just works and doesn't caues any problem. */}} | ||||
| 											<div class="bar bar-behind" style="width: {{Eval 100 "*" .CommitsBehind "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div> | ||||
| 										</div> | ||||
| 										<div class="bar-group"> | ||||
| 											<div class="count count-ahead">{{.CommitsAhead}}</div> | ||||
| 											<div class="bar bar-ahead" style="width: {{Eval 100 "*" .CommitsAhead "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div> | ||||
| 										</div> | ||||
| 									</div> | ||||
| 									{{end}} | ||||
| 								</td> | ||||
| 								<td class="two wide right aligned"> | ||||
| 									{{if not .LatestPullRequest}} | ||||
| 										{{if .IsIncluded}} | ||||
| 											<span class="ui orange large label" data-tooltip-content="{{$.locale.Tr "repo.branch.included_desc"}}"> | ||||
| 												{{svg "octicon-git-pull-request"}} {{$.locale.Tr "repo.branch.included"}} | ||||
| 											</span> | ||||
| 										{{else if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}} | ||||
| 										<a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .DBBranch.Name}}"> | ||||
| 											<button id="new-pull-request" class="ui compact basic button gt-mr-0">{{if $.CanPull}}{{$.locale.Tr "repo.pulls.compare_changes"}}{{else}}{{$.locale.Tr "action.compare_branch"}}{{end}}</button> | ||||
| 										</a> | ||||
| 										{{end}} | ||||
| 									{{else if and .LatestPullRequest.HasMerged .MergeMovedOn}} | ||||
| 										{{if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}} | ||||
| 										<a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{$.Owner.Name}}:{{end}}{{.Name | PathEscapeSegments}}"> | ||||
| 											<button id="new-pull-request" class="ui compact basic button gt-mr-0">{{if $.CanPull}}{{$.locale.Tr "repo.pulls.compare_changes"}}{{else}}{{$.locale.Tr "action.compare_branch"}}{{end}}</button> | ||||
| 										</a> | ||||
| 										{{end}} | ||||
| 									{{else}} | ||||
| 										<a href="{{.LatestPullRequest.Issue.Link}}" class="gt-vm ref-issue">{{if not .LatestPullRequest.IsSameRepo}}{{.LatestPullRequest.BaseRepo.FullName}}{{end}}#{{.LatestPullRequest.Issue.Index}}</a> | ||||
| 										{{if .LatestPullRequest.HasMerged}} | ||||
| 											<a href="{{.LatestPullRequest.Issue.Link}}" class="ui text-label purple large label gt-vm">{{svg "octicon-git-merge" 16 "gt-mr-2"}}{{$.locale.Tr "repo.pulls.merged"}}</a> | ||||
| 										{{else if .LatestPullRequest.Issue.IsClosed}} | ||||
| 											<a href="{{.LatestPullRequest.Issue.Link}}" class="ui text-label red large label gt-vm">{{svg "octicon-git-pull-request" 16 "gt-mr-2"}}{{$.locale.Tr "repo.issues.closed_title"}}</a> | ||||
| 										{{else}} | ||||
| 											<a href="{{.LatestPullRequest.Issue.Link}}" class="ui text-label green large label gt-vm">{{svg "octicon-git-pull-request" 16 "gt-mr-2"}}{{$.locale.Tr "repo.issues.open_title"}}</a> | ||||
| 										{{end}} | ||||
| 									{{end}} | ||||
| 								</td> | ||||
| 								<td class="three wide right aligned overflow-visible"> | ||||
| 									{{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted)}} | ||||
| 										<button class="btn interact-bg gt-p-3 show-modal show-create-branch-modal" | ||||
| 											data-branch-from="{{.DBBranch.Name}}" | ||||
| 											data-branch-from-urlcomponent="{{PathEscapeSegments .DBBranch.Name}}" | ||||
| 											data-tooltip-content="{{$.locale.Tr "repo.branch.new_branch_from" .DBBranch.Name}}" | ||||
| 											data-modal="#create-branch-modal" data-name="{{.DBBranch.Name}}" | ||||
| 										> | ||||
| 											{{svg "octicon-git-branch"}} | ||||
| 										</button> | ||||
| 									{{end}} | ||||
| 									{{if $.EnableFeed}} | ||||
| 										<a role="button" class="btn interact-bg gt-p-3" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .DBBranch.Name}}">{{svg "octicon-rss"}}</a> | ||||
| 									{{end}} | ||||
| 									{{if and (not .DBBranch.IsDeleted) (not $.DisableDownloadSourceArchives)}} | ||||
| 										<div class="ui dropdown btn interact-bg gt-p-3" data-tooltip-content="{{$.locale.Tr "repo.branch.download" (.DBBranch.Name)}}"> | ||||
| 											{{svg "octicon-download"}} | ||||
| 											<div class="menu"> | ||||
| 												<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments .DBBranch.Name}}.zip" rel="nofollow">{{svg "octicon-file-zip"}} ZIP</a> | ||||
| 												<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments .DBBranch.Name}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}} TAR.GZ</a> | ||||
| 											</div> | ||||
| 										</div> | ||||
| 										{{end}} | ||||
| 									</td> | ||||
| 									<td class="three wide right aligned"> | ||||
| 										{{if not .LatestPullRequest}} | ||||
| 											{{if .IsIncluded}} | ||||
| 												<span class="ui orange large label" data-tooltip-content="{{$.locale.Tr "repo.branch.included_desc"}}"> | ||||
| 													{{svg "octicon-git-pull-request"}} {{$.locale.Tr "repo.branch.included"}} | ||||
| 									{{end}} | ||||
| 									{{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted) (not $.IsMirror)}} | ||||
| 										<button class="btn interact-bg gt-p-3 show-modal show-rename-branch-modal" | ||||
| 											data-is-default-branch="false" | ||||
| 											data-old-branch-name="{{.DBBranch.Name}}" | ||||
| 											data-modal="#rename-branch-modal" | ||||
| 											data-tooltip-content="{{$.locale.Tr "repo.branch.rename" (.DBBranch.Name)}}" | ||||
| 										> | ||||
| 											{{svg "octicon-pencil"}} | ||||
| 										</button> | ||||
| 									{{end}} | ||||
| 									{{if and $.IsWriter (not $.IsMirror) (not $.Repository.IsArchived) (not .IsProtected)}} | ||||
| 										{{if .DBBranch.IsDeleted}} | ||||
| 											<button class="btn interact-bg gt-p-3 link-action restore-branch-button" data-url="{{$.Link}}/restore?branch_id={{.DBBranch.ID}}&name={{.DBBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{$.locale.Tr "repo.branch.restore" (.DBBranch.Name)}}"> | ||||
| 												<span class="text blue"> | ||||
| 													{{svg "octicon-reply"}} | ||||
| 												</span> | ||||
| 											{{else if and (not .IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}} | ||||
| 											<a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranch}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .Name}}"> | ||||
| 												<button id="new-pull-request" class="ui compact basic button gt-mr-0">{{if $.CanPull}}{{$.locale.Tr "repo.pulls.compare_changes"}}{{else}}{{$.locale.Tr "action.compare_branch"}}{{end}}</button> | ||||
| 											</a> | ||||
| 											{{end}} | ||||
| 										{{else if and .LatestPullRequest.HasMerged .MergeMovedOn}} | ||||
| 											{{if and (not .IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}} | ||||
| 											<a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranch}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{$.Owner.Name}}:{{end}}{{.Name | PathEscapeSegments}}"> | ||||
| 												<button id="new-pull-request" class="ui compact basic button gt-mr-0">{{if $.CanPull}}{{$.locale.Tr "repo.pulls.compare_changes"}}{{else}}{{$.locale.Tr "action.compare_branch"}}{{end}}</button> | ||||
| 											</a> | ||||
| 											{{end}} | ||||
| 											</button> | ||||
| 										{{else}} | ||||
| 											<a href="{{.LatestPullRequest.Issue.Link}}" class="gt-vm ref-issue">{{if not .LatestPullRequest.IsSameRepo}}{{.LatestPullRequest.BaseRepo.FullName}}{{end}}#{{.LatestPullRequest.Issue.Index}}</a> | ||||
| 											{{if .LatestPullRequest.HasMerged}} | ||||
| 												<a href="{{.LatestPullRequest.Issue.Link}}" class="ui text-label purple large label gt-vm">{{svg "octicon-git-merge" 16 "gt-mr-2"}}{{$.locale.Tr "repo.pulls.merged"}}</a> | ||||
| 											{{else if .LatestPullRequest.Issue.IsClosed}} | ||||
| 												<a href="{{.LatestPullRequest.Issue.Link}}" class="ui text-label red large label gt-vm">{{svg "octicon-git-pull-request" 16 "gt-mr-2"}}{{$.locale.Tr "repo.issues.closed_title"}}</a> | ||||
| 											{{else}} | ||||
| 												<a href="{{.LatestPullRequest.Issue.Link}}" class="ui text-label green large label gt-vm">{{svg "octicon-git-pull-request" 16 "gt-mr-2"}}{{$.locale.Tr "repo.issues.open_title"}}</a> | ||||
| 											{{end}} | ||||
| 										{{end}} | ||||
| 									</td> | ||||
| 									<td class="three wide right aligned overflow-visible"> | ||||
| 										{{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted)}} | ||||
| 											<button class="btn interact-bg gt-p-3 show-modal show-create-branch-modal" | ||||
| 												data-branch-from="{{.Name}}" | ||||
| 												data-branch-from-urlcomponent="{{PathEscapeSegments .Name}}" | ||||
| 												data-tooltip-content="{{$.locale.Tr "repo.branch.new_branch_from" .Name}}" | ||||
| 												data-modal="#create-branch-modal" data-name="{{.Name}}" | ||||
| 											> | ||||
| 												{{svg "octicon-git-branch"}} | ||||
| 											<button class="btn interact-bg gt-p-3 delete-button delete-branch-button" data-url="{{$.Link}}/delete?name={{.DBBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{$.locale.Tr "repo.branch.delete" (.DBBranch.Name)}}" data-name="{{.DBBranch.Name}}"> | ||||
| 												{{svg "octicon-trash"}} | ||||
| 											</button> | ||||
| 										{{end}} | ||||
| 										{{if $.EnableFeed}} | ||||
| 											<a role="button" class="btn interact-bg gt-p-3" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .Name}}">{{svg "octicon-rss"}}</a> | ||||
| 										{{end}} | ||||
| 										{{if and (not .IsDeleted) (not $.DisableDownloadSourceArchives)}} | ||||
| 											<div class="ui dropdown btn interact-bg gt-p-3" data-tooltip-content="{{$.locale.Tr "repo.branch.download" (.Name)}}"> | ||||
| 												{{svg "octicon-download"}} | ||||
| 												<div class="menu"> | ||||
| 													<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments .Name}}.zip" rel="nofollow">{{svg "octicon-file-zip"}} ZIP</a> | ||||
| 													<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments .Name}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}} TAR.GZ</a> | ||||
| 												</div> | ||||
| 											</div> | ||||
| 										{{end}} | ||||
| 										{{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted) (not $.IsMirror)}} | ||||
| 											<button class="btn interact-bg gt-p-3 show-modal show-rename-branch-modal" | ||||
| 												data-is-default-branch="false" | ||||
| 												data-old-branch-name="{{.Name}}" | ||||
| 												data-modal="#rename-branch-modal" | ||||
| 												data-tooltip-content="{{$.locale.Tr "repo.branch.rename" (.Name)}}" | ||||
| 											> | ||||
| 												{{svg "octicon-pencil"}} | ||||
| 											</button> | ||||
| 										{{end}} | ||||
| 										{{if and $.IsWriter (not $.IsMirror) (not $.Repository.IsArchived) (not .IsProtected)}} | ||||
| 											{{if .IsDeleted}} | ||||
| 												<button class="btn interact-bg gt-p-3 link-action restore-branch-button" data-url="{{$.Link}}/restore?branch_id={{.DeletedBranch.ID}}&name={{.DeletedBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{$.locale.Tr "repo.branch.restore" (.Name)}}"> | ||||
| 													<span class="text blue"> | ||||
| 														{{svg "octicon-reply"}} | ||||
| 													</span> | ||||
| 												</button> | ||||
| 											{{else}} | ||||
| 												<button class="btn interact-bg gt-p-3 delete-button delete-branch-button" data-url="{{$.Link}}/delete?name={{.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{$.locale.Tr "repo.branch.delete" (.Name)}}" data-name="{{.Name}}"> | ||||
| 													{{svg "octicon-trash"}} | ||||
| 												</button> | ||||
| 											{{end}} | ||||
| 										{{end}} | ||||
| 									</td> | ||||
| 								</tr> | ||||
| 							{{end}} | ||||
| 									{{end}} | ||||
| 								</td> | ||||
| 							</tr> | ||||
| 						{{end}} | ||||
| 					</tbody> | ||||
| 				</table> | ||||
|   | ||||
| @@ -7,13 +7,19 @@ import ( | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
|  | ||||
| 	git_model "code.gitea.io/gitea/models/git" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	"code.gitea.io/gitea/models/unittest" | ||||
| 	"code.gitea.io/gitea/tests" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestRenameBranch(t *testing.T) { | ||||
| 	defer tests.PrepareTestEnv(t)() | ||||
|  | ||||
| 	unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: 1, Name: "master"}) | ||||
|  | ||||
| 	// get branch setting page | ||||
| 	session := loginUser(t, "user2") | ||||
| 	req := NewRequest(t, "GET", "/user2/repo1/settings/branches") | ||||
|   | ||||
		Reference in New Issue
	
	Block a user