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>tags/v1.21.0-rc0
@@ -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 |
@@ -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 | |||
} |
@@ -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 |
@@ -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() |
@@ -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> | |||
<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}} | |||
<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}} | |||
{{if .IsProtected}} | |||
{{svg "octicon-shield-lock"}} | |||
<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}} | |||
<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> | |||
{{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> | |||
</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}} | |||
{{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> | |||
{{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"}} | |||
{{else}} | |||
<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 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") |