mirror of https://github.com/go-gitea/gitea
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>pull/25582/head^2
parent
5a871932f0
commit
6e19484f4d
@ -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 |
|
@ -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() |
||||||
|
} |
@ -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 |
||||||
|
} |
@ -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() |
|
||||||
} |
|
@ -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") |
||||||
|
} |
@ -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 |
||||||
|
} |
Loading…
Reference in new issue