mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-30 15:43:52 +03:00
286d09203f
Unlike other async processing in the queue, we should sync branches to the DB immediately when handling git hook calling. If it fails, users can see the error message in the output of the git command. It can avoid potential inconsistency issues, and help #29494. --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
408 lines
11 KiB
Go
408 lines
11 KiB
Go
// 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/git"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/timeutil"
|
|
"code.gitea.io/gitea/modules/util"
|
|
|
|
"xorm.io/builder"
|
|
)
|
|
|
|
// 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"` // git's ref-name is case-sensitive internally, however, in some databases (mssql, mysql, by default), it's case-insensitive at the moment
|
|
CommitID string
|
|
CommitMessage string `xorm:"TEXT"` // it only stores the message summary (the first line)
|
|
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) GetRepo(ctx context.Context) (*repo_model.Repository, error) {
|
|
return repo_model.GetRepositoryByID(ctx, b.RepoID)
|
|
}
|
|
|
|
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 GetBranches(ctx context.Context, repoID int64, branchNames []string) ([]*Branch, error) {
|
|
branches := make([]*Branch, 0, len(branchNames))
|
|
return branches, db.GetEngine(ctx).Where("repo_id=?", repoID).In("name", branchNames).Find(&branches)
|
|
}
|
|
|
|
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.
|
|
func UpdateBranch(ctx context.Context, repoID, pusherID int64, branchName string, commit *git.Commit) (int64, error) {
|
|
return 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: commit.ID.String(),
|
|
CommitMessage: commit.Summary(),
|
|
PusherID: pusherID,
|
|
CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()),
|
|
IsDeleted: false,
|
|
})
|
|
}
|
|
|
|
// 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)
|
|
|
|
var branch Branch
|
|
exist, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repo.ID, from).Get(&branch)
|
|
if err != nil {
|
|
return err
|
|
} else if !exist || branch.IsDeleted {
|
|
return ErrBranchNotExist{
|
|
RepoID: repo.ID,
|
|
BranchName: from,
|
|
}
|
|
}
|
|
|
|
// 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]any{"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()
|
|
}
|
|
|
|
// FindRecentlyPushedNewBranches return at most 2 new branches pushed by the user in 6 hours which has no opened PRs created
|
|
// except the indicate branch
|
|
func FindRecentlyPushedNewBranches(ctx context.Context, repoID, userID int64, excludeBranchName string) (BranchList, error) {
|
|
branches := make(BranchList, 0, 2)
|
|
subQuery := builder.Select("head_branch").From("pull_request").
|
|
InnerJoin("issue", "issue.id = pull_request.issue_id").
|
|
Where(builder.Eq{
|
|
"pull_request.head_repo_id": repoID,
|
|
"issue.is_closed": false,
|
|
})
|
|
err := db.GetEngine(ctx).
|
|
Where("pusher_id=? AND is_deleted=?", userID, false).
|
|
And("name <> ?", excludeBranchName).
|
|
And("repo_id = ?", repoID).
|
|
And("commit_time >= ?", time.Now().Add(-time.Hour*6).Unix()).
|
|
NotIn("name", subQuery).
|
|
OrderBy("branch.commit_time DESC").
|
|
Limit(2).
|
|
Find(&branches)
|
|
return branches, err
|
|
}
|