Sync branches into databases (#22743)

Related #14180
Related #25233 
Related #22639
Close #19786
Related #12763 

This PR will change all the branches retrieve method from reading git
data to read database to reduce git read operations.

- [x] Sync git branches information into database when push git data
- [x] Create a new table `Branch`, merge some columns of `DeletedBranch`
into `Branch` table and drop the table `DeletedBranch`.
- [x] Read `Branch` table when visit `code` -> `branch` page
- [x] Read `Branch` table when list branch names in `code` page dropdown
- [x] Read `Branch` table when list git ref compare page
- [x] Provide a button in admin page to manually sync all branches.
- [x] Sync branches if repository is not empty but database branches are
empty when visiting pages with branches list
- [x] Use `commit_time desc` as the default FindBranch order by to keep
consistent as before and deleted branches will be always at the end.

---------

Co-authored-by: Jason Song <i@wolfogre.com>
This commit is contained in:
Lunny Xiao 2023-06-29 18:03:20 +08:00 committed by GitHub
parent 5a871932f0
commit 6e19484f4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 1416 additions and 724 deletions

View file

@ -318,90 +318,6 @@ func (err ErrFilePathProtected) Unwrap() error {
return util.ErrPermissionDenied 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. // ErrDisallowedToMerge represents an error that a branch is protected and the current user is not allowed to modify it.
type ErrDisallowedToMerge struct { type ErrDisallowedToMerge struct {
Reason string Reason string

View file

@ -0,0 +1,47 @@
-
id: 1
repo_id: 1
name: 'foo'
commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d'
commit_message: 'first commit'
commit_time: 978307100
pusher_id: 1
is_deleted: true
deleted_by_id: 1
deleted_unix: 978307200
-
id: 2
repo_id: 1
name: 'bar'
commit_id: '62fb502a7172d4453f0322a2cc85bddffa57f07a'
commit_message: 'second commit'
commit_time: 978307100
pusher_id: 1
is_deleted: true
deleted_by_id: 99
deleted_unix: 978307200
-
id: 3
repo_id: 1
name: 'branch2'
commit_id: '985f0301dba5e7b34be866819cd15ad3d8f508ee'
commit_message: 'make pull5 outdated'
commit_time: 1579166279
pusher_id: 1
is_deleted: false
deleted_by_id: 0
deleted_unix: 0
-
id: 4
repo_id: 1
name: 'master'
commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d'
commit_message: 'Initial commit'
commit_time: 1489927679
pusher_id: 1
is_deleted: false
deleted_by_id: 0
deleted_unix: 0

View file

@ -1,15 +0,0 @@
-
id: 1
repo_id: 1
name: foo
commit: 1213212312313213213132131
deleted_by_id: 1
deleted_unix: 978307200
-
id: 2
repo_id: 1
name: bar
commit: 5655464564554545466464655
deleted_by_id: 99
deleted_unix: 978307200

379
models/git/branch.go Normal file
View file

@ -0,0 +1,379 @@
// Copyright 2016 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"context"
"fmt"
"time"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
)
// ErrBranchNotExist represents an error that branch with such name does not exist.
type ErrBranchNotExist struct {
RepoID int64
BranchName string
}
// IsErrBranchNotExist checks if an error is an ErrBranchDoesNotExist.
func IsErrBranchNotExist(err error) bool {
_, ok := err.(ErrBranchNotExist)
return ok
}
func (err ErrBranchNotExist) Error() string {
return fmt.Sprintf("branch does not exist [repo_id: %d name: %s]", err.RepoID, err.BranchName)
}
func (err ErrBranchNotExist) Unwrap() error {
return util.ErrNotExist
}
// ErrBranchAlreadyExists represents an error that branch with such name already exists.
type ErrBranchAlreadyExists struct {
BranchName string
}
// IsErrBranchAlreadyExists checks if an error is an ErrBranchAlreadyExists.
func IsErrBranchAlreadyExists(err error) bool {
_, ok := err.(ErrBranchAlreadyExists)
return ok
}
func (err ErrBranchAlreadyExists) Error() string {
return fmt.Sprintf("branch already exists [name: %s]", err.BranchName)
}
func (err ErrBranchAlreadyExists) Unwrap() error {
return util.ErrAlreadyExist
}
// ErrBranchNameConflict represents an error that branch name conflicts with other branch.
type ErrBranchNameConflict struct {
BranchName string
}
// IsErrBranchNameConflict checks if an error is an ErrBranchNameConflict.
func IsErrBranchNameConflict(err error) bool {
_, ok := err.(ErrBranchNameConflict)
return ok
}
func (err ErrBranchNameConflict) Error() string {
return fmt.Sprintf("branch conflicts with existing branch [name: %s]", err.BranchName)
}
func (err ErrBranchNameConflict) Unwrap() error {
return util.ErrAlreadyExist
}
// ErrBranchesEqual represents an error that base branch is equal to the head branch.
type ErrBranchesEqual struct {
BaseBranchName string
HeadBranchName string
}
// IsErrBranchesEqual checks if an error is an ErrBranchesEqual.
func IsErrBranchesEqual(err error) bool {
_, ok := err.(ErrBranchesEqual)
return ok
}
func (err ErrBranchesEqual) Error() string {
return fmt.Sprintf("branches are equal [head: %sm base: %s]", err.HeadBranchName, err.BaseBranchName)
}
func (err ErrBranchesEqual) Unwrap() error {
return util.ErrInvalidArgument
}
// Branch represents a branch of a repository
// For those repository who have many branches, stored into database is a good choice
// for pagination, keyword search and filtering
type Branch struct {
ID int64
RepoID int64 `xorm:"UNIQUE(s)"`
Name string `xorm:"UNIQUE(s) NOT NULL"`
CommitID string
CommitMessage string `xorm:"TEXT"`
PusherID int64
Pusher *user_model.User `xorm:"-"`
IsDeleted bool `xorm:"index"`
DeletedByID int64
DeletedBy *user_model.User `xorm:"-"`
DeletedUnix timeutil.TimeStamp `xorm:"index"`
CommitTime timeutil.TimeStamp // The commit
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
func (b *Branch) LoadDeletedBy(ctx context.Context) (err error) {
if b.DeletedBy == nil {
b.DeletedBy, err = user_model.GetUserByID(ctx, b.DeletedByID)
if user_model.IsErrUserNotExist(err) {
b.DeletedBy = user_model.NewGhostUser()
err = nil
}
}
return err
}
func (b *Branch) LoadPusher(ctx context.Context) (err error) {
if b.Pusher == nil && b.PusherID > 0 {
b.Pusher, err = user_model.GetUserByID(ctx, b.PusherID)
if user_model.IsErrUserNotExist(err) {
b.Pusher = user_model.NewGhostUser()
err = nil
}
}
return err
}
func init() {
db.RegisterModel(new(Branch))
db.RegisterModel(new(RenamedBranch))
}
func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, error) {
var branch Branch
has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).And("name=?", branchName).Get(&branch)
if err != nil {
return nil, err
} else if !has {
return nil, ErrBranchNotExist{
RepoID: repoID,
BranchName: branchName,
}
}
return &branch, nil
}
func AddBranches(ctx context.Context, branches []*Branch) error {
for _, branch := range branches {
if _, err := db.GetEngine(ctx).Insert(branch); err != nil {
return err
}
}
return nil
}
func GetDeletedBranchByID(ctx context.Context, repoID, branchID int64) (*Branch, error) {
var branch Branch
has, err := db.GetEngine(ctx).ID(branchID).Get(&branch)
if err != nil {
return nil, err
} else if !has {
return nil, ErrBranchNotExist{
RepoID: repoID,
}
}
if branch.RepoID != repoID {
return nil, ErrBranchNotExist{
RepoID: repoID,
}
}
if !branch.IsDeleted {
return nil, ErrBranchNotExist{
RepoID: repoID,
}
}
return &branch, nil
}
func DeleteBranches(ctx context.Context, repoID, doerID int64, branchIDs []int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
branches := make([]*Branch, 0, len(branchIDs))
if err := db.GetEngine(ctx).In("id", branchIDs).Find(&branches); err != nil {
return err
}
for _, branch := range branches {
if err := AddDeletedBranch(ctx, repoID, branch.Name, doerID); err != nil {
return err
}
}
return nil
})
}
// UpdateBranch updates the branch information in the database. If the branch exist, it will update latest commit of this branch information
// If it doest not exist, insert a new record into database
func UpdateBranch(ctx context.Context, repoID int64, branchName, commitID, commitMessage string, pusherID int64, commitTime time.Time) error {
cnt, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repoID, branchName).
Cols("commit_id, commit_message, pusher_id, commit_time, is_deleted, updated_unix").
Update(&Branch{
CommitID: commitID,
CommitMessage: commitMessage,
PusherID: pusherID,
CommitTime: timeutil.TimeStamp(commitTime.Unix()),
IsDeleted: false,
})
if err != nil {
return err
}
if cnt > 0 {
return nil
}
return db.Insert(ctx, &Branch{
RepoID: repoID,
Name: branchName,
CommitID: commitID,
CommitMessage: commitMessage,
PusherID: pusherID,
CommitTime: timeutil.TimeStamp(commitTime.Unix()),
})
}
// AddDeletedBranch adds a deleted branch to the database
func AddDeletedBranch(ctx context.Context, repoID int64, branchName string, deletedByID int64) error {
branch, err := GetBranch(ctx, repoID, branchName)
if err != nil {
return err
}
if branch.IsDeleted {
return nil
}
cnt, err := db.GetEngine(ctx).Where("repo_id=? AND name=? AND is_deleted=?", repoID, branchName, false).
Cols("is_deleted, deleted_by_id, deleted_unix").
Update(&Branch{
IsDeleted: true,
DeletedByID: deletedByID,
DeletedUnix: timeutil.TimeStampNow(),
})
if err != nil {
return err
}
if cnt == 0 {
return fmt.Errorf("branch %s not found or has been deleted", branchName)
}
return err
}
func RemoveDeletedBranchByID(ctx context.Context, repoID, branchID int64) error {
_, err := db.GetEngine(ctx).Where("repo_id=? AND id=? AND is_deleted = ?", repoID, branchID, true).Delete(new(Branch))
return err
}
// RemoveOldDeletedBranches removes old deleted branches
func RemoveOldDeletedBranches(ctx context.Context, olderThan time.Duration) {
// Nothing to do for shutdown or terminate
log.Trace("Doing: DeletedBranchesCleanup")
deleteBefore := time.Now().Add(-olderThan)
_, err := db.GetEngine(ctx).Where("is_deleted=? AND deleted_unix < ?", true, deleteBefore.Unix()).Delete(new(Branch))
if err != nil {
log.Error("DeletedBranchesCleanup: %v", err)
}
}
// RenamedBranch provide renamed branch log
// will check it when a branch can't be found
type RenamedBranch struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX NOT NULL"`
From string
To string
CreatedUnix timeutil.TimeStamp `xorm:"created"`
}
// FindRenamedBranch check if a branch was renamed
func FindRenamedBranch(ctx context.Context, repoID int64, from string) (branch *RenamedBranch, exist bool, err error) {
branch = &RenamedBranch{
RepoID: repoID,
From: from,
}
exist, err = db.GetEngine(ctx).Get(branch)
return branch, exist, err
}
// RenameBranch rename a branch
func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to string, gitAction func(isDefault bool) error) (err error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
sess := db.GetEngine(ctx)
// 1. update branch in database
if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Update(&Branch{
Name: to,
}); err != nil {
return err
} else if n <= 0 {
return ErrBranchNotExist{
RepoID: repo.ID,
BranchName: from,
}
}
// 2. update default branch if needed
isDefault := repo.DefaultBranch == from
if isDefault {
repo.DefaultBranch = to
_, err = sess.ID(repo.ID).Cols("default_branch").Update(repo)
if err != nil {
return err
}
}
// 3. Update protected branch if needed
protectedBranch, err := GetProtectedBranchRuleByName(ctx, repo.ID, from)
if err != nil {
return err
}
if protectedBranch != nil {
// there is a protect rule for this branch
protectedBranch.RuleName = to
_, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch)
if err != nil {
return err
}
} else {
// some glob protect rules may match this branch
protected, err := IsBranchProtected(ctx, repo.ID, from)
if err != nil {
return err
}
if protected {
return ErrBranchIsProtected
}
}
// 4. Update all not merged pull request base branch name
_, err = sess.Table("pull_request").Where("base_repo_id=? AND base_branch=? AND has_merged=?",
repo.ID, from, false).
Update(map[string]interface{}{"base_branch": to})
if err != nil {
return err
}
// 5. do git action
if err = gitAction(isDefault); err != nil {
return err
}
// 6. insert renamed branch record
renamedBranch := &RenamedBranch{
RepoID: repo.ID,
From: from,
To: to,
}
err = db.Insert(ctx, renamedBranch)
if err != nil {
return err
}
return committer.Commit()
}

132
models/git/branch_list.go Normal file
View file

@ -0,0 +1,132 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"context"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
"xorm.io/xorm"
)
type BranchList []*Branch
func (branches BranchList) LoadDeletedBy(ctx context.Context) error {
ids := container.Set[int64]{}
for _, branch := range branches {
if !branch.IsDeleted {
continue
}
ids.Add(branch.DeletedByID)
}
usersMap := make(map[int64]*user_model.User, len(ids))
if err := db.GetEngine(ctx).In("id", ids.Values()).Find(&usersMap); err != nil {
return err
}
for _, branch := range branches {
if !branch.IsDeleted {
continue
}
branch.DeletedBy = usersMap[branch.DeletedByID]
if branch.DeletedBy == nil {
branch.DeletedBy = user_model.NewGhostUser()
}
}
return nil
}
func (branches BranchList) LoadPusher(ctx context.Context) error {
ids := container.Set[int64]{}
for _, branch := range branches {
if branch.PusherID > 0 { // pusher_id maybe zero because some branches are sync by backend with no pusher
ids.Add(branch.PusherID)
}
}
usersMap := make(map[int64]*user_model.User, len(ids))
if err := db.GetEngine(ctx).In("id", ids.Values()).Find(&usersMap); err != nil {
return err
}
for _, branch := range branches {
if branch.PusherID <= 0 {
continue
}
branch.Pusher = usersMap[branch.PusherID]
if branch.Pusher == nil {
branch.Pusher = user_model.NewGhostUser()
}
}
return nil
}
const (
BranchOrderByNameAsc = "name ASC"
BranchOrderByCommitTimeDesc = "commit_time DESC"
)
type FindBranchOptions struct {
db.ListOptions
RepoID int64
ExcludeBranchNames []string
IsDeletedBranch util.OptionalBool
OrderBy string
}
func (opts *FindBranchOptions) Cond() builder.Cond {
cond := builder.NewCond()
if opts.RepoID > 0 {
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
}
if len(opts.ExcludeBranchNames) > 0 {
cond = cond.And(builder.NotIn("name", opts.ExcludeBranchNames))
}
if !opts.IsDeletedBranch.IsNone() {
cond = cond.And(builder.Eq{"is_deleted": opts.IsDeletedBranch.IsTrue()})
}
return cond
}
func CountBranches(ctx context.Context, opts FindBranchOptions) (int64, error) {
return db.GetEngine(ctx).Where(opts.Cond()).Count(&Branch{})
}
func orderByBranches(sess *xorm.Session, opts FindBranchOptions) *xorm.Session {
if !opts.IsDeletedBranch.IsFalse() { // if deleted branch included, put them at the end
sess = sess.OrderBy("is_deleted ASC")
}
if opts.OrderBy == "" {
opts.OrderBy = BranchOrderByCommitTimeDesc
}
return sess.OrderBy(opts.OrderBy)
}
func FindBranches(ctx context.Context, opts FindBranchOptions) (BranchList, error) {
sess := db.GetEngine(ctx).Where(opts.Cond())
if opts.PageSize > 0 && !opts.IsListAll() {
sess = db.SetSessionPagination(sess, &opts.ListOptions)
}
sess = orderByBranches(sess, opts)
var branches []*Branch
return branches, sess.Find(&branches)
}
func FindBranchNames(ctx context.Context, opts FindBranchOptions) ([]string, error) {
sess := db.GetEngine(ctx).Select("name").Where(opts.Cond())
if opts.PageSize > 0 && !opts.IsListAll() {
sess = db.SetSessionPagination(sess, &opts.ListOptions)
}
sess = orderByBranches(sess, opts)
var branches []string
if err := sess.Table("branch").Find(&branches); err != nil {
return nil, err
}
return branches, nil
}

View file

@ -11,6 +11,7 @@ import (
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -18,24 +19,37 @@ import (
func TestAddDeletedBranch(t *testing.T) { func TestAddDeletedBranch(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) 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.True(t, firstBranch.IsDeleted)
assert.NoError(t, git_model.AddDeletedBranch(db.DefaultContext, repo.ID, "test", "5655464564554545466464656", int64(1))) 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) { func TestGetDeletedBranches(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) 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.NoError(t, err)
assert.Len(t, branches, 2) assert.Len(t, branches, 2)
} }
func TestGetDeletedBranch(t *testing.T) { func TestGetDeletedBranch(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) 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)) assert.NotNil(t, getDeletedBranch(t, firstBranch))
} }
@ -43,18 +57,18 @@ func TestGetDeletedBranch(t *testing.T) {
func TestDeletedBranchLoadUser(t *testing.T) { func TestDeletedBranchLoadUser(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.DeletedBranch{ID: 1}) firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 1})
secondBranch := unittest.AssertExistsAndLoadBean(t, &git_model.DeletedBranch{ID: 2}) secondBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 2})
branch := getDeletedBranch(t, firstBranch) branch := getDeletedBranch(t, firstBranch)
assert.Nil(t, branch.DeletedBy) assert.Nil(t, branch.DeletedBy)
branch.LoadUser(db.DefaultContext) branch.LoadDeletedBy(db.DefaultContext)
assert.NotNil(t, branch.DeletedBy) assert.NotNil(t, branch.DeletedBy)
assert.Equal(t, "user1", branch.DeletedBy.Name) assert.Equal(t, "user1", branch.DeletedBy.Name)
branch = getDeletedBranch(t, secondBranch) branch = getDeletedBranch(t, secondBranch)
assert.Nil(t, branch.DeletedBy) assert.Nil(t, branch.DeletedBy)
branch.LoadUser(db.DefaultContext) branch.LoadDeletedBy(db.DefaultContext)
assert.NotNil(t, branch.DeletedBy) assert.NotNil(t, branch.DeletedBy)
assert.Equal(t, "Ghost", branch.DeletedBy.Name) assert.Equal(t, "Ghost", branch.DeletedBy.Name)
} }
@ -63,22 +77,22 @@ func TestRemoveDeletedBranch(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) 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) err := git_model.RemoveDeletedBranchByID(db.DefaultContext, repo.ID, 1)
assert.NoError(t, err) assert.NoError(t, err)
unittest.AssertNotExistsBean(t, firstBranch) 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}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
deletedBranch, err := git_model.GetDeletedBranchByID(db.DefaultContext, repo.ID, branch.ID) deletedBranch, err := git_model.GetDeletedBranchByID(db.DefaultContext, repo.ID, branch.ID)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, branch.ID, deletedBranch.ID) assert.Equal(t, branch.ID, deletedBranch.ID)
assert.Equal(t, branch.Name, deletedBranch.Name) 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) assert.Equal(t, branch.DeletedByID, deletedBranch.DeletedByID)
return deletedBranch return deletedBranch
@ -146,8 +160,8 @@ func TestOnlyGetDeletedBranchOnCorrectRepo(t *testing.T) {
deletedBranch, err := git_model.GetDeletedBranchByID(db.DefaultContext, repo2.ID, 1) deletedBranch, err := git_model.GetDeletedBranchByID(db.DefaultContext, repo2.ID, 1)
// Expect no error, and the returned branch is nil. // Expect error, and the returned branch is nil.
assert.NoError(t, err) assert.Error(t, err)
assert.Nil(t, deletedBranch) assert.Nil(t, deletedBranch)
// Now get the deletedBranch with ID of 1 on repo with ID 1. // Now get the deletedBranch with ID of 1 on repo with ID 1.

View file

@ -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()
}

View file

@ -8,7 +8,7 @@ import (
"sort" "sort"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/util"
"github.com/gobwas/glob" "github.com/gobwas/glob"
) )
@ -47,19 +47,32 @@ func FindRepoProtectedBranchRules(ctx context.Context, repoID int64) (ProtectedB
} }
// FindAllMatchedBranches find all matched branches // FindAllMatchedBranches find all matched branches
func FindAllMatchedBranches(ctx context.Context, gitRepo *git.Repository, ruleName string) ([]string, error) { func FindAllMatchedBranches(ctx context.Context, repoID int64, ruleName string) ([]string, error) {
// FIXME: how many should we get? results := make([]string, 0, 10)
branches, _, err := gitRepo.GetBranchNames(0, 9999999) for page := 1; ; page++ {
if err != nil { brancheNames, err := FindBranchNames(ctx, FindBranchOptions{
return nil, err ListOptions: db.ListOptions{
} PageSize: 100,
rule := glob.MustCompile(ruleName) Page: page,
results := make([]string, 0, len(branches)) },
for _, branch := range branches { RepoID: repoID,
if rule.Match(branch) { IsDeletedBranch: util.OptionalBoolFalse,
results = append(results, branch) })
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 return results, nil
} }

View file

@ -509,6 +509,8 @@ var migrations = []Migration{
NewMigration("Add TriggerEvent to action_run table", v1_21.AddTriggerEventToActionRun), NewMigration("Add TriggerEvent to action_run table", v1_21.AddTriggerEventToActionRun),
// v263 -> v264 // v263 -> v264
NewMigration("Add git_size and lfs_size columns to repository table", v1_21.AddGitSizeAndLFSSizeToRepositoryTable), 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 // GetCurrentDBVersion returns the current db version

View file

@ -0,0 +1,93 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_21 //nolint
import (
"context"
"fmt"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm"
)
func AddBranchTable(x *xorm.Engine) error {
type Branch struct {
ID int64
RepoID int64 `xorm:"UNIQUE(s)"`
Name string `xorm:"UNIQUE(s) NOT NULL"`
CommitID string
CommitMessage string `xorm:"TEXT"`
PusherID int64
IsDeleted bool `xorm:"index"`
DeletedByID int64
DeletedUnix timeutil.TimeStamp `xorm:"index"`
CommitTime timeutil.TimeStamp // The commit
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
if err := x.Sync(new(Branch)); err != nil {
return err
}
if exist, err := x.IsTableExist("deleted_branches"); err != nil {
return err
} else if !exist {
return nil
}
type DeletedBranch struct {
ID int64
RepoID int64 `xorm:"index UNIQUE(s)"`
Name string `xorm:"UNIQUE(s) NOT NULL"`
Commit string
DeletedByID int64
DeletedUnix timeutil.TimeStamp
}
var adminUserID int64
has, err := x.Table("user").
Select("id").
Where("is_admin=?", true).
Asc("id"). // Reliably get the admin with the lowest ID.
Get(&adminUserID)
if err != nil {
return err
} else if !has {
return fmt.Errorf("no admin user found")
}
branches := make([]Branch, 0, 100)
if err := db.Iterate(context.Background(), nil, func(ctx context.Context, deletedBranch *DeletedBranch) error {
branches = append(branches, Branch{
RepoID: deletedBranch.RepoID,
Name: deletedBranch.Name,
CommitID: deletedBranch.Commit,
PusherID: adminUserID,
IsDeleted: true,
DeletedByID: deletedBranch.DeletedByID,
DeletedUnix: deletedBranch.DeletedUnix,
})
if len(branches) >= 100 {
_, err := x.Insert(&branches)
if err != nil {
return err
}
branches = branches[:0]
}
return nil
}); err != nil {
return err
}
if len(branches) > 0 {
if _, err := x.Insert(&branches); err != nil {
return err
}
}
return x.DropTables("deleted_branches")
}

View file

@ -147,7 +147,7 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error {
&repo_model.Collaboration{RepoID: repoID}, &repo_model.Collaboration{RepoID: repoID},
&issues_model.Comment{RefRepoID: repoID}, &issues_model.Comment{RefRepoID: repoID},
&git_model.CommitStatus{RepoID: repoID}, &git_model.CommitStatus{RepoID: repoID},
&git_model.DeletedBranch{RepoID: repoID}, &git_model.Branch{RepoID: repoID},
&git_model.LFSLock{RepoID: repoID}, &git_model.LFSLock{RepoID: repoID},
&repo_model.LanguageStat{RepoID: repoID}, &repo_model.LanguageStat{RepoID: repoID},
&issues_model.Milestone{RepoID: repoID}, &issues_model.Milestone{RepoID: repoID},

View file

@ -1171,9 +1171,9 @@ func GetUserByOpenID(uri string) (*User, error) {
} }
// GetAdminUser returns the first administrator // GetAdminUser returns the first administrator
func GetAdminUser() (*User, error) { func GetAdminUser(ctx context.Context) (*User, error) {
var admin User var admin User
has, err := db.GetEngine(db.DefaultContext). has, err := db.GetEngine(ctx).
Where("is_admin=?", true). Where("is_admin=?", true).
Asc("id"). // Reliably get the admin with the lowest ID. Asc("id"). // Reliably get the admin with the lowest ID.
Get(&admin) Get(&admin)

View file

@ -667,13 +667,38 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) {
} }
ctx.Data["Tags"] = tags 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 { if err != nil {
ctx.ServerError("GetBranches", err) ctx.ServerError("GetBranches", err)
return return
} }
ctx.Data["Branches"] = brs // always put default branch on the top
ctx.Data["BranchesCount"] = len(brs) ctx.Data["Branches"] = append(branchOpts.ExcludeBranchNames, brs...)
ctx.Data["BranchesCount"] = branchesTotal
// If not branch selected, try default one. // If not branch selected, try default one.
// If default branch doesn't exist, fall back to some other branch. // 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 { if len(ctx.Params("*")) == 0 {
refName = ctx.Repo.Repository.DefaultBranch refName = ctx.Repo.Repository.DefaultBranch
if !ctx.Repo.GitRepo.IsBranchExist(refName) { 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 { if err == nil && len(brs) != 0 {
refName = brs[0] refName = brs[0].Name
} else if len(brs) == 0 { } else if len(brs) == 0 {
log.Error("No branches in non-empty repository %s", ctx.Repo.GitRepo.Path) log.Error("No branches in non-empty repository %s", ctx.Repo.GitRepo.Path)
ctx.Repo.Repository.MarkAsBrokenEmpty() ctx.Repo.Repository.MarkAsBrokenEmpty()

View file

@ -0,0 +1,135 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
import (
"context"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
)
// SyncRepoBranches synchronizes branch table with repository branches
func SyncRepoBranches(ctx context.Context, repoID, doerID int64) (int64, error) {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
return 0, err
}
log.Debug("SyncRepoBranches: in Repo[%d:%s]", repo.ID, repo.FullName())
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
if err != nil {
log.Error("OpenRepository[%s]: %w", repo.RepoPath(), err)
return 0, err
}
defer gitRepo.Close()
return SyncRepoBranchesWithRepo(ctx, repo, gitRepo, doerID)
}
func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doerID int64) (int64, error) {
allBranches := container.Set[string]{}
{
branches, _, err := gitRepo.GetBranchNames(0, 0)
if err != nil {
return 0, err
}
log.Trace("SyncRepoBranches[%s]: branches[%d]: %v", repo.FullName(), len(branches), branches)
for _, branch := range branches {
allBranches.Add(branch)
}
}
dbBranches := make(map[string]*git_model.Branch)
{
branches, err := git_model.FindBranches(ctx, git_model.FindBranchOptions{
ListOptions: db.ListOptions{
ListAll: true,
},
RepoID: repo.ID,
})
if err != nil {
return 0, err
}
for _, branch := range branches {
dbBranches[branch.Name] = branch
}
}
var toAdd []*git_model.Branch
var toUpdate []*git_model.Branch
var toRemove []int64
for branch := range allBranches {
dbb := dbBranches[branch]
commit, err := gitRepo.GetBranchCommit(branch)
if err != nil {
return 0, err
}
if dbb == nil {
toAdd = append(toAdd, &git_model.Branch{
RepoID: repo.ID,
Name: branch,
CommitID: commit.ID.String(),
CommitMessage: commit.CommitMessage,
PusherID: doerID,
CommitTime: timeutil.TimeStamp(commit.Author.When.Unix()),
})
} else if commit.ID.String() != dbb.CommitID {
toUpdate = append(toUpdate, &git_model.Branch{
ID: dbb.ID,
RepoID: repo.ID,
Name: branch,
CommitID: commit.ID.String(),
CommitMessage: commit.CommitMessage,
PusherID: doerID,
CommitTime: timeutil.TimeStamp(commit.Author.When.Unix()),
})
}
}
for _, dbBranch := range dbBranches {
if !allBranches.Contains(dbBranch.Name) && !dbBranch.IsDeleted {
toRemove = append(toRemove, dbBranch.ID)
}
}
log.Trace("SyncRepoBranches[%s]: toAdd: %v, toUpdate: %v, toRemove: %v", repo.FullName(), toAdd, toUpdate, toRemove)
if len(toAdd) == 0 && len(toRemove) == 0 && len(toUpdate) == 0 {
return int64(len(allBranches)), nil
}
if err := db.WithTx(ctx, func(subCtx context.Context) error {
if len(toAdd) > 0 {
if err := git_model.AddBranches(subCtx, toAdd); err != nil {
return err
}
}
for _, b := range toUpdate {
if _, err := db.GetEngine(subCtx).ID(b.ID).
Cols("commit_id, commit_message, pusher_id, commit_time, is_deleted").
Update(b); err != nil {
return err
}
}
if len(toRemove) > 0 {
if err := git_model.DeleteBranches(subCtx, repo.ID, doerID, toRemove); err != nil {
return err
}
}
return nil
}); err != nil {
return 0, err
}
return int64(len(allBranches)), nil
}

View file

@ -351,6 +351,12 @@ func initRepository(ctx context.Context, repoPath string, u *user_model.User, re
if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
return fmt.Errorf("setDefaultBranch: %w", err) 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 { if err = UpdateRepository(ctx, repo, false); err != nil {

View file

@ -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 { if !opts.Releases {
// note: this will greatly improve release (tag) sync // note: this will greatly improve release (tag) sync
// for pull-mirrors with many tags // 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 { if err != nil {
return nil, err return nil, err
} }

View file

@ -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 = Delete all repositories missing their Git files
dashboard.delete_missing_repos.started = Delete all repositories missing their Git files task started. dashboard.delete_missing_repos.started = Delete all repositories missing their Git files task started.
dashboard.delete_generated_repository_avatars = Delete generated repository avatars 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.update_mirrors = Update Mirrors
dashboard.repo_health_check = Health check all repositories dashboard.repo_health_check = Health check all repositories
dashboard.check_repo_stats = Check all repository statistics 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_zombie_tasks = Stop zombie tasks
dashboard.stop_endless_tasks = Stop endless tasks dashboard.stop_endless_tasks = Stop endless tasks
dashboard.cancel_abandoned_jobs = Cancel abandoned jobs dashboard.cancel_abandoned_jobs = Cancel abandoned jobs
dashboard.sync_branch.started = Branches Sync started
users.user_manage_panel = User Account Management users.user_manage_panel = User Account Management
users.new_account = Create User Account users.new_account = Create User Account

View file

@ -15,7 +15,9 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
repo_module "code.gitea.io/gitea/modules/repository"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
@ -76,7 +78,7 @@ func GetBranch(ctx *context.APIContext) {
return 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 { if err != nil {
ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err) ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err)
return return
@ -118,6 +120,37 @@ func DeleteBranch(ctx *context.APIContext) {
branchName := ctx.Params("*") 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 { if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil {
switch { switch {
case git.IsErrBranchNotExist(err): 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) err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, oldCommit.ID.String(), opt.BranchName)
if err != nil { if err != nil {
if models.IsErrBranchDoesNotExist(err) { if git_model.IsErrBranchNotExist(err) {
ctx.Error(http.StatusNotFound, "", "The old branch does not exist") ctx.Error(http.StatusNotFound, "", "The old branch does not exist")
} }
if models.IsErrTagAlreadyExists(err) { if models.IsErrTagAlreadyExists(err) {
ctx.Error(http.StatusConflict, "", "The branch with the same tag already exists.") 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.") 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.") ctx.Error(http.StatusConflict, "", "The branch with the same name already exists.")
} else { } else {
ctx.Error(http.StatusInternalServerError, "CreateNewBranchFromCommit", err) ctx.Error(http.StatusInternalServerError, "CreateNewBranchFromCommit", err)
@ -236,7 +269,7 @@ func CreateBranch(ctx *context.APIContext) {
return 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 { if err != nil {
ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err) ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err)
return return
@ -275,20 +308,38 @@ func ListBranches(ctx *context.APIContext) {
// "200": // "200":
// "$ref": "#/responses/BranchList" // "$ref": "#/responses/BranchList"
var totalNumOfBranches int var totalNumOfBranches int64
var apiBranches []*api.Branch var apiBranches []*api.Branch
listOptions := utils.GetListOptions(ctx) listOptions := utils.GetListOptions(ctx)
if !ctx.Repo.Repository.IsEmpty && ctx.Repo.GitRepo != nil { 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) rules, err := git_model.FindRepoProtectedBranchRules(ctx, ctx.Repo.Repository.ID)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "FindMatchedProtectedBranchRules", err) ctx.Error(http.StatusInternalServerError, "FindMatchedProtectedBranchRules", err)
return return
} }
skip, _ := listOptions.GetStartEnd() branches, err := git_model.FindBranches(ctx, branchOpts)
branches, total, err := ctx.Repo.GitRepo.GetBranches(skip, listOptions.PageSize)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "GetBranches", err) ctx.Error(http.StatusInternalServerError, "GetBranches", err)
return return
@ -296,11 +347,11 @@ func ListBranches(ctx *context.APIContext) {
apiBranches = make([]*api.Branch, 0, len(branches)) apiBranches = make([]*api.Branch, 0, len(branches))
for i := range branches { for i := range branches {
c, err := branches[i].GetCommit() c, err := ctx.Repo.GitRepo.GetBranchCommit(branches[i].Name)
if err != nil { if err != nil {
// Skip if this branch doesn't exist anymore. // Skip if this branch doesn't exist anymore.
if git.IsErrNotExist(err) { if git.IsErrNotExist(err) {
total-- totalNumOfBranches--
continue continue
} }
ctx.Error(http.StatusInternalServerError, "GetCommit", err) ctx.Error(http.StatusInternalServerError, "GetCommit", err)
@ -308,19 +359,17 @@ func ListBranches(ctx *context.APIContext) {
} }
branchProtection := rules.GetFirstMatched(branches[i].Name) 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 { if err != nil {
ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err) ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err)
return return
} }
apiBranches = append(apiBranches, apiBranch) apiBranches = append(apiBranches, apiBranch)
} }
totalNumOfBranches = total
} }
ctx.SetLinkHeader(totalNumOfBranches, listOptions.PageSize) ctx.SetLinkHeader(int(totalNumOfBranches), listOptions.PageSize)
ctx.SetTotalCountHeader(int64(totalNumOfBranches)) ctx.SetTotalCountHeader(totalNumOfBranches)
ctx.JSON(http.StatusOK, apiBranches) 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 // 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 { if err != nil {
ctx.Error(http.StatusInternalServerError, "FindAllMatchedBranches", err) ctx.Error(http.StatusInternalServerError, "FindAllMatchedBranches", err)
return return
@ -851,7 +900,7 @@ func EditBranchProtection(ctx *context.APIContext) {
} }
// FIXME: since we only need to recheck files protected rules, we could improve this // 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 { if err != nil {
ctx.Error(http.StatusInternalServerError, "FindAllMatchedBranches", err) ctx.Error(http.StatusInternalServerError, "FindAllMatchedBranches", err)
return return

View file

@ -687,12 +687,12 @@ func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) {
ctx.Error(http.StatusForbidden, "Access", err) ctx.Error(http.StatusForbidden, "Access", err)
return 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) { models.IsErrFilePathInvalid(err) || models.IsErrRepoFileAlreadyExists(err) {
ctx.Error(http.StatusUnprocessableEntity, "Invalid", err) ctx.Error(http.StatusUnprocessableEntity, "Invalid", err)
return return
} }
if models.IsErrBranchDoesNotExist(err) || git.IsErrBranchNotExist(err) { if git_model.IsErrBranchNotExist(err) || git.IsErrBranchNotExist(err) {
ctx.Error(http.StatusNotFound, "BranchDoesNotExist", err) ctx.Error(http.StatusNotFound, "BranchDoesNotExist", err)
return return
} }
@ -843,7 +843,7 @@ func DeleteFile(ctx *context.APIContext) {
if git.IsErrBranchNotExist(err) || models.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) { if git.IsErrBranchNotExist(err) || models.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) {
ctx.Error(http.StatusNotFound, "DeleteFile", err) ctx.Error(http.StatusNotFound, "DeleteFile", err)
return return
} else if models.IsErrBranchAlreadyExists(err) || } else if git_model.IsErrBranchAlreadyExists(err) ||
models.IsErrFilenameInvalid(err) || models.IsErrFilenameInvalid(err) ||
models.IsErrSHADoesNotMatch(err) || models.IsErrSHADoesNotMatch(err) ||
models.IsErrCommitIDDoesNotMatch(err) || models.IsErrCommitIDDoesNotMatch(err) ||

View file

@ -8,6 +8,7 @@ import (
"time" "time"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
@ -91,12 +92,12 @@ func ApplyDiffPatch(ctx *context.APIContext) {
ctx.Error(http.StatusForbidden, "Access", err) ctx.Error(http.StatusForbidden, "Access", err)
return 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) { models.IsErrFilePathInvalid(err) || models.IsErrRepoFileAlreadyExists(err) {
ctx.Error(http.StatusUnprocessableEntity, "Invalid", err) ctx.Error(http.StatusUnprocessableEntity, "Invalid", err)
return return
} }
if models.IsErrBranchDoesNotExist(err) || git.IsErrBranchNotExist(err) { if git_model.IsErrBranchNotExist(err) || git.IsErrBranchNotExist(err) {
ctx.Error(http.StatusNotFound, "BranchDoesNotExist", err) ctx.Error(http.StatusNotFound, "BranchDoesNotExist", err)
return return
} }

View file

@ -14,12 +14,15 @@ import (
activities_model "code.gitea.io/gitea/models/activities" activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/updatechecker" "code.gitea.io/gitea/modules/updatechecker"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/cron" "code.gitea.io/gitea/services/cron"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
repo_service "code.gitea.io/gitea/services/repository"
) )
const ( const (
@ -133,12 +136,22 @@ func DashboardPost(ctx *context.Context) {
// Run operation. // Run operation.
if form.Op != "" { if form.Op != "" {
task := cron.GetTask(form.Op) switch form.Op {
if task != nil { case "sync_repo_branches":
go task.RunWithUser(ctx.Doer, nil) go func() {
ctx.Flash.Success(ctx.Tr("admin.dashboard.task.started", ctx.Tr("admin.dashboard."+form.Op))) if err := repo_service.AddAllRepoBranchesToSyncQueue(graceful.GetManager().ShutdownContext(), ctx.Doer.ID); err != nil {
} else { log.Error("AddAllRepoBranchesToSyncQueue: %v: %v", ctx.Doer.ID, err)
ctx.Flash.Error(ctx.Tr("admin.dashboard.task.unknown", form.Op)) }
}()
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" { if form.From == "monitor" {

View file

@ -13,7 +13,6 @@ import (
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
@ -28,32 +27,16 @@ import (
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
release_service "code.gitea.io/gitea/services/release" release_service "code.gitea.io/gitea/services/release"
repo_service "code.gitea.io/gitea/services/repository" repo_service "code.gitea.io/gitea/services/repository"
files_service "code.gitea.io/gitea/services/repository/files"
) )
const ( const (
tplBranch base.TplName = "repo/branch/list" 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 // Branches render repository branch page
func Branches(ctx *context.Context) { func Branches(ctx *context.Context) {
ctx.Data["Title"] = "Branches" ctx.Data["Title"] = "Branches"
ctx.Data["IsRepoToolbarBranches"] = true ctx.Data["IsRepoToolbarBranches"] = true
ctx.Data["DefaultBranch"] = ctx.Repo.Repository.DefaultBranch
ctx.Data["AllowsPulls"] = ctx.Repo.Repository.AllowsPulls() ctx.Data["AllowsPulls"] = ctx.Repo.Repository.AllowsPulls()
ctx.Data["IsWriter"] = ctx.Repo.CanWrite(unit.TypeCode) ctx.Data["IsWriter"] = ctx.Repo.CanWrite(unit.TypeCode)
ctx.Data["IsMirror"] = ctx.Repo.Repository.IsMirror ctx.Data["IsMirror"] = ctx.Repo.Repository.IsMirror
@ -68,15 +51,15 @@ func Branches(ctx *context.Context) {
} }
pageSize := setting.Git.BranchesRangeSize pageSize := setting.Git.BranchesRangeSize
skip := (page - 1) * pageSize defaultBranch, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, util.OptionalBoolNone, page, pageSize)
log.Debug("Branches: skip: %d limit: %d", skip, pageSize) if err != nil {
defaultBranchBranch, branches, branchesCount := loadBranches(ctx, skip, pageSize) ctx.ServerError("LoadBranches", err)
if ctx.Written() {
return return
} }
ctx.Data["Branches"] = branches ctx.Data["Branches"] = branches
ctx.Data["DefaultBranchBranch"] = defaultBranchBranch ctx.Data["DefaultBranchBranch"] = defaultBranch
pager := context.NewPagination(branchesCount, pageSize, page, 5) pager := context.NewPagination(int(branchesCount), pageSize, page, 5)
pager.SetDefaultParams(ctx) pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager ctx.Data["Page"] = pager
@ -130,7 +113,7 @@ func RestoreBranchPost(ctx *context.Context) {
if err := git.Push(ctx, ctx.Repo.Repository.RepoPath(), git.PushOptions{ if err := git.Push(ctx, ctx.Repo.Repository.RepoPath(), git.PushOptions{
Remote: ctx.Repo.Repository.RepoPath(), 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), Env: repo_module.PushingEnvironment(ctx.Doer, ctx.Repo.Repository),
}); err != nil { }); err != nil {
if strings.Contains(err.Error(), "already exists") { if strings.Contains(err.Error(), "already exists") {
@ -148,7 +131,7 @@ func RestoreBranchPost(ctx *context.Context) {
&repo_module.PushUpdateOptions{ &repo_module.PushUpdateOptions{
RefFullName: git.RefNameFromBranch(deletedBranch.Name), RefFullName: git.RefNameFromBranch(deletedBranch.Name),
OldCommitID: git.EmptySHA, OldCommitID: git.EmptySHA,
NewCommitID: deletedBranch.Commit, NewCommitID: deletedBranch.CommitID,
PusherID: ctx.Doer.ID, PusherID: ctx.Doer.ID,
PusherName: ctx.Doer.Name, PusherName: ctx.Doer.Name,
RepoUserName: ctx.Repo.Owner.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 // CreateBranch creates new branch in repository
func CreateBranch(ctx *context.Context) { func CreateBranch(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.NewBranchForm) form := web.GetForm(ctx).(*forms.NewBranchForm)
@ -380,13 +189,13 @@ func CreateBranch(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
return 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.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.NewBranchName))
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
return return
} }
if models.IsErrBranchNameConflict(err) { if git_model.IsErrBranchNameConflict(err) {
e := err.(models.ErrBranchNameConflict) e := err.(git_model.ErrBranchNameConflict)
ctx.Flash.Error(ctx.Tr("repo.branch.branch_name_conflict", form.NewBranchName, e.BranchName)) ctx.Flash.Error(ctx.Tr("repo.branch.branch_name_conflict", form.NewBranchName, e.BranchName))
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
return return

View file

@ -9,6 +9,7 @@ import (
"strings" "strings"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context" "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 // First lets try the simple plain read-tree -m approach
opts.Content = sha opts.Content = sha
if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, form.Revert, opts); err != nil { 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 // User has specified a branch that already exists
branchErr := err.(models.ErrBranchAlreadyExists) branchErr := err.(git_model.ErrBranchAlreadyExists)
ctx.Data["Err_NewBranchName"] = true ctx.Data["Err_NewBranchName"] = true
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form) ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form)
return return
@ -161,9 +162,9 @@ func CherryPickPost(ctx *context.Context) {
ctx.Data["FileContent"] = opts.Content ctx.Data["FileContent"] = opts.Content
if _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil { 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 // User has specified a branch that already exists
branchErr := err.(models.ErrBranchAlreadyExists) branchErr := err.(git_model.ErrBranchAlreadyExists)
ctx.Data["Err_NewBranchName"] = true ctx.Data["Err_NewBranchName"] = true
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form) ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form)
return return

View file

@ -16,6 +16,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access" 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() 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 { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -734,7 +741,13 @@ func CompareDiff(ctx *context.Context) {
return 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 { if err != nil {
ctx.ServerError("GetBranches", err) ctx.ServerError("GetBranches", err)
return return

View file

@ -327,10 +327,10 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
} else { } else {
ctx.Error(http.StatusInternalServerError, err.Error()) 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 // For when a user specifies a new branch that already exists
ctx.Data["Err_NewBranchName"] = true 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) ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
} else { } else {
ctx.Error(http.StatusInternalServerError, err.Error()) ctx.Error(http.StatusInternalServerError, err.Error())
@ -529,9 +529,9 @@ func DeleteFilePost(ctx *context.Context) {
} else { } else {
ctx.Error(http.StatusInternalServerError, err.Error()) 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 // 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) ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplDeleteFile, &form)
} else { } else {
ctx.Error(http.StatusInternalServerError, err.Error()) ctx.Error(http.StatusInternalServerError, err.Error())
@ -731,10 +731,10 @@ func UploadFilePost(ctx *context.Context) {
} else if git.IsErrBranchNotExist(err) { } else if git.IsErrBranchNotExist(err) {
branchErr := err.(git.ErrBranchNotExist) branchErr := err.(git.ErrBranchNotExist)
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplUploadFile, &form) 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 // For when a user specifies a new branch that already exists
ctx.Data["Err_NewBranchName"] = true 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) ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplUploadFile, &form)
} else if git.IsErrPushOutOfDate(err) { } 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) 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)

View file

@ -785,7 +785,13 @@ func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull
return nil 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 { if err != nil {
ctx.ServerError("GetBranches", err) ctx.ServerError("GetBranches", err)
return nil return nil

View file

@ -7,6 +7,7 @@ import (
"strings" "strings"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
@ -94,9 +95,9 @@ func NewDiffPatchPost(ctx *context.Context) {
Content: strings.ReplaceAll(form.Content, "\r", ""), Content: strings.ReplaceAll(form.Content, "\r", ""),
}) })
if err != nil { if err != nil {
if models.IsErrBranchAlreadyExists(err) { if git_model.IsErrBranchAlreadyExists(err) {
// User has specified a branch that already exists // User has specified a branch that already exists
branchErr := err.(models.ErrBranchAlreadyExists) branchErr := err.(git_model.ErrBranchAlreadyExists)
ctx.Data["Err_NewBranchName"] = true ctx.Data["Err_NewBranchName"] = true
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form) ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
return return

View file

@ -1493,7 +1493,7 @@ func UpdatePullRequestTarget(ctx *context.Context) {
"error": err.Error(), "error": err.Error(),
"user_error": errorMessage, "user_error": errorMessage,
}) })
} else if models.IsErrBranchesEqual(err) { } else if git_model.IsErrBranchesEqual(err) {
errorMessage := ctx.Tr("repo.pulls.nothing_to_compare") errorMessage := ctx.Tr("repo.pulls.nothing_to_compare")
ctx.Flash.Error(errorMessage) ctx.Flash.Error(errorMessage)

View file

@ -286,7 +286,7 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
} }
// FIXME: since we only need to recheck files protected rules, we could improve this // 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 { if err != nil {
ctx.ServerError("FindAllMatchedBranches", err) ctx.ServerError("FindAllMatchedBranches", err)
return return

View file

@ -50,7 +50,7 @@ func ToEmailSearch(email *user_model.SearchEmailResult) *api.Email {
} }
// ToBranch convert a git.Commit and git.Branch to an api.Branch // 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 { if bp == nil {
var hasPerm bool var hasPerm bool
var canPush bool var canPush bool
@ -65,11 +65,11 @@ func ToBranch(ctx context.Context, repo *repo_model.Repository, b *git.Branch, c
if err != nil { if err != nil {
return nil, err return nil, err
} }
canPush = issues_model.CanMaintainerWriteToBranch(perms, b.Name, user) canPush = issues_model.CanMaintainerWriteToBranch(perms, branchName, user)
} }
return &api.Branch{ return &api.Branch{
Name: b.Name, Name: branchName,
Commit: ToPayloadCommit(ctx, repo, c), Commit: ToPayloadCommit(ctx, repo, c),
Protected: false, Protected: false,
RequiredApprovals: 0, RequiredApprovals: 0,
@ -81,7 +81,7 @@ func ToBranch(ctx context.Context, repo *repo_model.Repository, b *git.Branch, c
} }
branch := &api.Branch{ branch := &api.Branch{
Name: b.Name, Name: branchName,
Commit: ToPayloadCommit(ctx, repo, c), Commit: ToPayloadCommit(ctx, repo, c),
Protected: true, Protected: true,
RequiredApprovals: bp.RequiredApprovals, RequiredApprovals: bp.RequiredApprovals,

View file

@ -642,7 +642,7 @@ func (g *RepositoryDumper) Finish() error {
// DumpRepository dump repository according MigrateOptions to a local directory // DumpRepository dump repository according MigrateOptions to a local directory
func DumpRepository(ctx context.Context, baseDir, ownerName string, opts base.MigrateOptions) error { 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 { if err != nil {
return err return err
} }
@ -705,7 +705,7 @@ func updateOptionsUnits(opts *base.MigrateOptions, units []string) error {
// RestoreRepository restore a repository from the disk directory // RestoreRepository restore a repository from the disk directory
func RestoreRepository(ctx context.Context, baseDir, ownerName, repoName string, units []string, validation bool) error { 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 { if err != nil {
return err return err
} }

View file

@ -170,7 +170,7 @@ func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer
return err return err
} }
if branchesEqual { if branchesEqual {
return models.ErrBranchesEqual{ return git_model.ErrBranchesEqual{
HeadBranchName: pr.HeadBranch, HeadBranchName: pr.HeadBranch,
BaseBranchName: targetBranch, BaseBranchName: targetBranch,
} }
@ -338,7 +338,7 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
for _, pr := range prs { for _, pr := range prs {
divergence, err := GetDiverging(ctx, pr) divergence, err := GetDiverging(ctx, pr)
if err != nil { 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) log.Warn("Cannot test PR %s/%d: head_branch %s no longer exists", pr.BaseRepo.Name, pr.IssueID, pr.HeadBranch)
} else { } else {
log.Error("GetDiverging: %v", err) log.Error("GetDiverging: %v", err)

View file

@ -11,7 +11,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"code.gitea.io/gitea/models" git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
@ -181,7 +181,7 @@ func createTemporaryRepoForPR(ctx context.Context, pr *issues_model.PullRequest)
Run(prCtx.RunOpts()); err != nil { Run(prCtx.RunOpts()); err != nil {
cancel() cancel()
if !git.IsBranchExist(ctx, pr.HeadRepo.RepoPath(), pr.HeadBranch) { if !git.IsBranchExist(ctx, pr.HeadRepo.RepoPath(), pr.HeadBranch) {
return nil, nil, models.ErrBranchDoesNotExist{ return nil, nil, git_model.ErrBranchNotExist{
BranchName: pr.HeadBranch, BranchName: pr.HeadBranch,
} }
} }

View file

@ -7,7 +7,6 @@ import (
"context" "context"
"fmt" "fmt"
"code.gitea.io/gitea/models"
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access" 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) log.Trace("GetDiverging[%-v]: compare commits", pr)
prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr) prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
if err != nil { if err != nil {
if !models.IsErrBranchDoesNotExist(err) { if !git_model.IsErrBranchNotExist(err) {
log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err) log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err)
} }
return nil, err return nil, err

View file

@ -12,6 +12,7 @@ import (
"strings" "strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container" "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 found := false
hasDefault := false hasDefault := false
hasMaster := false hasMaster := false

View file

@ -10,13 +10,21 @@ import (
"strings" "strings"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification" "code.gitea.io/gitea/modules/notification"
"code.gitea.io/gitea/modules/queue"
repo_module "code.gitea.io/gitea/modules/repository" 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 // 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) { if !git.IsBranchExist(ctx, repo.RepoPath(), oldBranchName) {
return models.ErrBranchDoesNotExist{ return git_model.ErrBranchNotExist{
BranchName: oldBranchName, 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) { if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
return err return err
} }
return fmt.Errorf("Push: %w", err) return fmt.Errorf("push: %w", err)
} }
return nil return nil
} }
// GetBranches returns branches from the repository, skipping skip initial branches and // Branch contains the branch information
// returning at most limit branches, or all branches if limit is 0. type Branch struct {
func GetBranches(ctx context.Context, repo *repo_model.Repository, skip, limit int) ([]*git.Branch, int, error) { DBBranch *git_model.Branch
return git.GetBranchesByPath(ctx, repo.RepoPath(), skip, limit) 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) { 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) branchRefName := strings.TrimPrefix(refName, git.BranchPrefix)
switch { switch {
case branchRefName == name: case branchRefName == name:
return models.ErrBranchAlreadyExists{ return git_model.ErrBranchAlreadyExists{
BranchName: name, BranchName: name,
} }
// If branchRefName like a/b but we want to create a branch named a then we have a conflict // If branchRefName like a/b but we want to create a branch named a then we have a conflict
case strings.HasPrefix(branchRefName, name+"/"): case strings.HasPrefix(branchRefName, name+"/"):
return models.ErrBranchNameConflict{ return git_model.ErrBranchNameConflict{
BranchName: branchRefName, BranchName: branchRefName,
} }
// Conversely if branchRefName like a but we want to create a branch named a/b then we also have a conflict // 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+"/"): case strings.HasPrefix(name, branchRefName+"/"):
return models.ErrBranchNameConflict{ return git_model.ErrBranchNameConflict{
BranchName: branchRefName, BranchName: branchRefName,
} }
case refName == git.TagPrefix+name: 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) { if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
return err return err
} }
return fmt.Errorf("Push: %w", err) return fmt.Errorf("push: %w", err)
} }
return nil return nil
@ -169,13 +326,28 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R
return git_model.ErrBranchIsProtected 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) commit, err := gitRepo.GetBranchCommit(branchName)
if err != nil { if err != nil {
return err return err
} }
if err := gitRepo.DeleteBranch(branchName, git.DeleteBranchOptions{ if err := db.WithTx(ctx, func(ctx context.Context) error {
Force: true, 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 { }); err != nil {
return err return err
} }
@ -196,3 +368,45 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R
return nil 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
}

View file

@ -58,7 +58,7 @@ func (opts *ApplyDiffPatchOptions) Validate(ctx context.Context, repo *repo_mode
if opts.NewBranch != opts.OldBranch { if opts.NewBranch != opts.OldBranch {
existingBranch, err := gitRepo.GetBranch(opts.NewBranch) existingBranch, err := gitRepo.GetBranch(opts.NewBranch)
if existingBranch != nil { if existingBranch != nil {
return models.ErrBranchAlreadyExists{ return git_model.ErrBranchAlreadyExists{
BranchName: opts.NewBranch, BranchName: opts.NewBranch,
} }
} }

View file

@ -197,7 +197,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
if opts.NewBranch != opts.OldBranch { if opts.NewBranch != opts.OldBranch {
existingBranch, err := gitRepo.GetBranch(opts.NewBranch) existingBranch, err := gitRepo.GetBranch(opts.NewBranch)
if existingBranch != nil { if existingBranch != nil {
return nil, models.ErrBranchAlreadyExists{ return nil, git_model.ErrBranchAlreadyExists{
BranchName: opts.NewBranch, BranchName: opts.NewBranch,
} }
} }

View file

@ -157,7 +157,15 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
if err = repo_module.CreateDelegateHooks(repoPath); err != nil { if err = repo_module.CreateDelegateHooks(repoPath); err != nil {
return fmt.Errorf("createDelegateHooks: %w", err) 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 needsRollbackInPanic = false
if err != nil { if err != nil {

View file

@ -93,7 +93,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
defer gitRepo.Close() defer gitRepo.Close()
if err = repo_module.UpdateRepoSize(ctx, repo); err != nil { 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)) addTags := make([]string, 0, len(optsList))
@ -259,8 +259,8 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
notification.NotifyPushCommits(ctx, pusher, repo, opts, commits) notification.NotifyPushCommits(ctx, pusher, repo, opts, commits)
if err = git_model.RemoveDeletedBranchByName(ctx, repo.ID, branch); err != nil { if err = git_model.UpdateBranch(ctx, repo.ID, branch, newCommit.ID.String(), newCommit.CommitMessage, opts.PusherID, newCommit.Committer.When); err != nil {
log.Error("models.RemoveDeletedBranch %s/%s failed: %v", repo.ID, branch, err) return fmt.Errorf("git_model.UpdateBranch %s:%s failed: %v", repo.FullName(), branch, err)
} }
// Cache for big repository // Cache for big repository
@ -273,8 +273,9 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
// close all related pulls // close all related pulls
log.Error("close related pull request failed: %v", err) 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)
} }
} }

View file

@ -17,6 +17,7 @@ import (
system_model "code.gitea.io/gitea/models/system" system_model "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" 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/log"
"code.gitea.io/gitea/modules/notification" "code.gitea.io/gitea/modules/notification"
repo_module "code.gitea.io/gitea/modules/repository" 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 repository uploads", setting.Repository.Upload.TempPath)
system_model.RemoveAllWithNotice(db.DefaultContext, "Clean up temporary repositories", repo_module.LocalCopyPath()) 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 // UpdateRepository updates a repository

View file

@ -61,6 +61,10 @@
<td>{{.locale.Tr "admin.dashboard.delete_generated_repository_avatars"}}</td> <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> <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>
<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> </tbody>
</table> </table>
</form> </form>

View file

@ -22,29 +22,29 @@
{{if .DefaultBranchBranch.IsProtected}} {{if .DefaultBranchBranch.IsProtected}}
{{svg "octicon-shield-lock"}} {{svg "octicon-shield-lock"}}
{{end}} {{end}}
<a href="{{.RepoLink}}/src/branch/{{PathEscapeSegments .DefaultBranch}}">{{.DefaultBranch}}</a> <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.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> <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}} &nbsp;{{template "shared/user/avatarlink" dict "Context" $.Context "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p>
</td> </td>
<td class="right aligned overflow-visible"> <td class="right aligned overflow-visible">
{{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted)}} {{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted)}}
<button class="btn interact-bg show-create-branch-modal gt-p-3" <button class="btn interact-bg show-create-branch-modal gt-p-3"
data-modal="#create-branch-modal" data-modal="#create-branch-modal"
data-branch-from="{{$.DefaultBranch}}" data-branch-from="{{$.DefaultBranchBranch}}"
data-branch-from-urlcomponent="{{PathEscapeSegments $.DefaultBranch}}" data-branch-from-urlcomponent="{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}"
data-tooltip-content="{{$.locale.Tr "repo.branch.new_branch_from" ($.DefaultBranch)}}" data-tooltip-content="{{$.locale.Tr "repo.branch.new_branch_from" ($.DefaultBranchBranch.DBBranch.Name)}}"
> >
{{svg "octicon-git-branch"}} {{svg "octicon-git-branch"}}
</button> </button>
{{end}} {{end}}
{{if .EnableFeed}} {{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}} {{end}}
{{if not $.DisableDownloadSourceArchives}} {{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"}} {{svg "octicon-download"}}
<div class="menu"> <div class="menu">
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.DefaultBranch}}.zip" rel="nofollow">{{svg "octicon-file-zip"}}&nbsp;ZIP</a> <a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}.zip" rel="nofollow">{{svg "octicon-file-zip"}}&nbsp;ZIP</a>
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.DefaultBranch}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}}&nbsp;TAR.GZ</a> <a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}}&nbsp;TAR.GZ</a>
</div> </div>
</div> </div>
{{end}} {{end}}
@ -52,8 +52,8 @@
<button class="btn interact-bg gt-p-3 show-modal show-rename-branch-modal" <button class="btn interact-bg gt-p-3 show-modal show-rename-branch-modal"
data-is-default-branch="true" data-is-default-branch="true"
data-modal="#rename-branch-modal" data-modal="#rename-branch-modal"
data-old-branch-name="{{$.DefaultBranch}}" data-old-branch-name="{{$.DefaultBranchBranch}}"
data-tooltip-content="{{$.locale.Tr "repo.branch.rename" ($.DefaultBranch)}}" data-tooltip-content="{{$.locale.Tr "repo.branch.rename" ($.DefaultBranchBranch.DBBranch.Name)}}"
> >
{{svg "octicon-pencil"}} {{svg "octicon-pencil"}}
</button> </button>
@ -65,7 +65,7 @@
</div> </div>
{{end}} {{end}}
{{if gt (len .Branches) 1}} {{if .Branches}}
<h4 class="ui top attached header"> <h4 class="ui top attached header">
{{.locale.Tr "repo.branches"}} {{.locale.Tr "repo.branches"}}
</h4> </h4>
@ -73,112 +73,110 @@
<table class="ui very basic striped fixed table single line"> <table class="ui very basic striped fixed table single line">
<tbody> <tbody>
{{range .Branches}} {{range .Branches}}
{{if ne .Name $.DefaultBranch}} <tr>
<tr> <td class="eight wide">
<td class="six wide"> {{if .DBBranch.IsDeleted}}
{{if .IsDeleted}} <s><a href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a></s>
<s><a href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .Name}}">{{.Name}}</a></s> <p class="info">{{$.locale.Tr "repo.branch.deleted_by" .DBBranch.DeletedBy.Name}} {{TimeSinceUnix .DBBranch.DeletedUnix $.locale}}</p>
<p class="info">{{$.locale.Tr "repo.branch.deleted_by" .DeletedBranch.DeletedBy.Name}} {{TimeSinceUnix .DeletedBranch.DeletedUnix $.locale}}</p> {{else}}
{{else}} {{if .IsProtected}}
{{if .IsProtected}} {{svg "octicon-shield-lock"}}
{{svg "octicon-shield-lock"}}
{{end}}
<a href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .Name}}">{{.Name}}</a>
<p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .Commit.ID.String}}">{{ShortSha .Commit.ID.String}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .Commit.CommitMessage $.RepoLink $.Repository.ComposeMetas}}</span> · {{$.locale.Tr "org.repo_updated"}} {{TimeSince .Commit.Committer.When $.locale}}</p>
{{end}} {{end}}
</td> <a href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a>
<td class="three wide ui"> <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}} &nbsp;{{template "shared/user/avatarlink" dict "Context" $.Context "user" .DBBranch.Pusher}} &nbsp;{{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p>
{{if and (not .IsDeleted) $.DefaultBranchBranch}} {{end}}
<div class="commit-divergence"> </td>
<div class="bar-group"> <td class="two wide ui">
<div class="count count-behind">{{.CommitsBehind}}</div> {{if and (not .DBBranch.IsDeleted) $.DefaultBranchBranch}}
{{/* 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="commit-divergence">
<div class="bar bar-behind" style="width: {{Eval 100 "*" .CommitsBehind "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div> <div class="bar-group">
</div> <div class="count count-behind">{{.CommitsBehind}}</div>
<div class="bar-group"> {{/* 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="count count-ahead">{{.CommitsAhead}}</div> <div class="bar bar-behind" style="width: {{Eval 100 "*" .CommitsBehind "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div>
<div class="bar bar-ahead" style="width: {{Eval 100 "*" .CommitsAhead "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div> </div>
<div class="bar-group">
<div class="count count-ahead">{{.CommitsAhead}}</div>
<div class="bar bar-ahead" style="width: {{Eval 100 "*" .CommitsAhead "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div>
</div>
</div>
{{end}}
</td>
<td class="two wide right aligned">
{{if not .LatestPullRequest}}
{{if .IsIncluded}}
<span class="ui orange large label" data-tooltip-content="{{$.locale.Tr "repo.branch.included_desc"}}">
{{svg "octicon-git-pull-request"}} {{$.locale.Tr "repo.branch.included"}}
</span>
{{else if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}}
<a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .DBBranch.Name}}">
<button id="new-pull-request" class="ui compact basic button gt-mr-0">{{if $.CanPull}}{{$.locale.Tr "repo.pulls.compare_changes"}}{{else}}{{$.locale.Tr "action.compare_branch"}}{{end}}</button>
</a>
{{end}}
{{else if and .LatestPullRequest.HasMerged .MergeMovedOn}}
{{if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}}
<a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{$.Owner.Name}}:{{end}}{{.Name | PathEscapeSegments}}">
<button id="new-pull-request" class="ui compact basic button gt-mr-0">{{if $.CanPull}}{{$.locale.Tr "repo.pulls.compare_changes"}}{{else}}{{$.locale.Tr "action.compare_branch"}}{{end}}</button>
</a>
{{end}}
{{else}}
<a href="{{.LatestPullRequest.Issue.Link}}" class="gt-vm ref-issue">{{if not .LatestPullRequest.IsSameRepo}}{{.LatestPullRequest.BaseRepo.FullName}}{{end}}#{{.LatestPullRequest.Issue.Index}}</a>
{{if .LatestPullRequest.HasMerged}}
<a href="{{.LatestPullRequest.Issue.Link}}" class="ui text-label purple large label gt-vm">{{svg "octicon-git-merge" 16 "gt-mr-2"}}{{$.locale.Tr "repo.pulls.merged"}}</a>
{{else if .LatestPullRequest.Issue.IsClosed}}
<a href="{{.LatestPullRequest.Issue.Link}}" class="ui text-label red large label gt-vm">{{svg "octicon-git-pull-request" 16 "gt-mr-2"}}{{$.locale.Tr "repo.issues.closed_title"}}</a>
{{else}}
<a href="{{.LatestPullRequest.Issue.Link}}" class="ui text-label green large label gt-vm">{{svg "octicon-git-pull-request" 16 "gt-mr-2"}}{{$.locale.Tr "repo.issues.open_title"}}</a>
{{end}}
{{end}}
</td>
<td class="three wide right aligned overflow-visible">
{{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted)}}
<button class="btn interact-bg gt-p-3 show-modal show-create-branch-modal"
data-branch-from="{{.DBBranch.Name}}"
data-branch-from-urlcomponent="{{PathEscapeSegments .DBBranch.Name}}"
data-tooltip-content="{{$.locale.Tr "repo.branch.new_branch_from" .DBBranch.Name}}"
data-modal="#create-branch-modal" data-name="{{.DBBranch.Name}}"
>
{{svg "octicon-git-branch"}}
</button>
{{end}}
{{if $.EnableFeed}}
<a role="button" class="btn interact-bg gt-p-3" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .DBBranch.Name}}">{{svg "octicon-rss"}}</a>
{{end}}
{{if and (not .DBBranch.IsDeleted) (not $.DisableDownloadSourceArchives)}}
<div class="ui dropdown btn interact-bg gt-p-3" data-tooltip-content="{{$.locale.Tr "repo.branch.download" (.DBBranch.Name)}}">
{{svg "octicon-download"}}
<div class="menu">
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments .DBBranch.Name}}.zip" rel="nofollow">{{svg "octicon-file-zip"}}&nbsp;ZIP</a>
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments .DBBranch.Name}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}}&nbsp;TAR.GZ</a>
</div> </div>
</div> </div>
{{end}} {{end}}
</td> {{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted) (not $.IsMirror)}}
<td class="three wide right aligned"> <button class="btn interact-bg gt-p-3 show-modal show-rename-branch-modal"
{{if not .LatestPullRequest}} data-is-default-branch="false"
{{if .IsIncluded}} data-old-branch-name="{{.DBBranch.Name}}"
<span class="ui orange large label" data-tooltip-content="{{$.locale.Tr "repo.branch.included_desc"}}"> data-modal="#rename-branch-modal"
{{svg "octicon-git-pull-request"}} {{$.locale.Tr "repo.branch.included"}} 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> </span>
{{else if and (not .IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}} </button>
<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}} {{else}}
<a href="{{.LatestPullRequest.Issue.Link}}" class="gt-vm ref-issue">{{if not .LatestPullRequest.IsSameRepo}}{{.LatestPullRequest.BaseRepo.FullName}}{{end}}#{{.LatestPullRequest.Issue.Index}}</a> <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}}">
{{if .LatestPullRequest.HasMerged}} {{svg "octicon-trash"}}
<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> </button>
{{end}} {{end}}
{{if $.EnableFeed}} {{end}}
<a role="button" class="btn interact-bg gt-p-3" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .Name}}">{{svg "octicon-rss"}}</a> </td>
{{end}} </tr>
{{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"}}&nbsp;ZIP</a>
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments .Name}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}}&nbsp;TAR.GZ</a>
</div>
</div>
{{end}}
{{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted) (not $.IsMirror)}}
<button class="btn interact-bg gt-p-3 show-modal show-rename-branch-modal"
data-is-default-branch="false"
data-old-branch-name="{{.Name}}"
data-modal="#rename-branch-modal"
data-tooltip-content="{{$.locale.Tr "repo.branch.rename" (.Name)}}"
>
{{svg "octicon-pencil"}}
</button>
{{end}}
{{if and $.IsWriter (not $.IsMirror) (not $.Repository.IsArchived) (not .IsProtected)}}
{{if .IsDeleted}}
<button class="btn interact-bg gt-p-3 link-action restore-branch-button" data-url="{{$.Link}}/restore?branch_id={{.DeletedBranch.ID}}&name={{.DeletedBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{$.locale.Tr "repo.branch.restore" (.Name)}}">
<span class="text blue">
{{svg "octicon-reply"}}
</span>
</button>
{{else}}
<button class="btn interact-bg gt-p-3 delete-button delete-branch-button" data-url="{{$.Link}}/delete?name={{.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{$.locale.Tr "repo.branch.delete" (.Name)}}" data-name="{{.Name}}">
{{svg "octicon-trash"}}
</button>
{{end}}
{{end}}
</td>
</tr>
{{end}}
{{end}} {{end}}
</tbody> </tbody>
</table> </table>

View file

@ -7,13 +7,19 @@ import (
"net/http" "net/http"
"testing" "testing"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestRenameBranch(t *testing.T) { func TestRenameBranch(t *testing.T) {
defer tests.PrepareTestEnv(t)()
unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: 1, Name: "master"})
// get branch setting page // get branch setting page
session := loginUser(t, "user2") session := loginUser(t, "user2")
req := NewRequest(t, "GET", "/user2/repo1/settings/branches") req := NewRequest(t, "GET", "/user2/repo1/settings/branches")