Skip to content

Commit d2a318b

Browse files
zeripathStelios Malathouras
authored and
Stelios Malathouras
committed
Improve checkBranchName (go-gitea#17901)
The current implementation of checkBranchName is highly inefficient involving opening the repository, the listing all of the branch names checking them individually before then using using opened repo to get the tags. This PR avoids this by simply walking the references from show-ref instead of opening the repository (in the nogogit case). Signed-off-by: Andrew Thornton <[email protected]>
1 parent 7467d7b commit d2a318b

File tree

10 files changed

+106
-58
lines changed

10 files changed

+106
-58
lines changed

modules/context/repo.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,7 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) {
584584
}
585585
ctx.Data["Tags"] = tags
586586

587-
brs, _, err := ctx.Repo.GitRepo.GetBranches(0, 0)
587+
brs, _, err := ctx.Repo.GitRepo.GetBranchNames(0, 0)
588588
if err != nil {
589589
ctx.ServerError("GetBranches", err)
590590
return
@@ -810,7 +810,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
810810
if len(ctx.Params("*")) == 0 {
811811
refName = ctx.Repo.Repository.DefaultBranch
812812
if !ctx.Repo.GitRepo.IsBranchExist(refName) {
813-
brs, _, err := ctx.Repo.GitRepo.GetBranches(0, 0)
813+
brs, _, err := ctx.Repo.GitRepo.GetBranchNames(0, 0)
814814
if err != nil {
815815
ctx.ServerError("GetBranches", err)
816816
return

modules/git/repo_branch.go

+8-3
Original file line numberDiff line numberDiff line change
@@ -95,17 +95,22 @@ func GetBranchesByPath(path string, skip, limit int) ([]*Branch, int, error) {
9595
}
9696
defer gitRepo.Close()
9797

98-
brs, countAll, err := gitRepo.GetBranches(skip, limit)
98+
return gitRepo.GetBranches(skip, limit)
99+
}
100+
101+
// GetBranches returns a slice of *git.Branch
102+
func (repo *Repository) GetBranches(skip, limit int) ([]*Branch, int, error) {
103+
brs, countAll, err := repo.GetBranchNames(skip, limit)
99104
if err != nil {
100105
return nil, 0, err
101106
}
102107

103108
branches := make([]*Branch, len(brs))
104109
for i := range brs {
105110
branches[i] = &Branch{
106-
Path: path,
111+
Path: repo.Path,
107112
Name: brs[i],
108-
gitRepo: gitRepo,
113+
gitRepo: repo,
109114
}
110115
}
111116

modules/git/repo_branch_gogit.go

+25-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
package git
1010

1111
import (
12+
"context"
1213
"strings"
1314

1415
"github.com/go-git/go-git/v5/plumbing"
@@ -52,7 +53,7 @@ func (repo *Repository) IsBranchExist(name string) bool {
5253

5354
// GetBranches returns branches from the repository, skipping skip initial branches and
5455
// returning at most limit branches, or all branches if limit is 0.
55-
func (repo *Repository) GetBranches(skip, limit int) ([]string, int, error) {
56+
func (repo *Repository) GetBranchNames(skip, limit int) ([]string, int, error) {
5657
var branchNames []string
5758

5859
branches, err := repo.gogitRepo.Branches()
@@ -79,3 +80,26 @@ func (repo *Repository) GetBranches(skip, limit int) ([]string, int, error) {
7980

8081
return branchNames, count, nil
8182
}
83+
84+
// WalkReferences walks all the references from the repository
85+
func WalkReferences(ctx context.Context, repoPath string, walkfn func(string) error) (int, error) {
86+
repo, err := OpenRepositoryCtx(ctx, repoPath)
87+
if err != nil {
88+
return 0, err
89+
}
90+
defer repo.Close()
91+
92+
i := 0
93+
iter, err := repo.gogitRepo.References()
94+
if err != nil {
95+
return i, err
96+
}
97+
defer iter.Close()
98+
99+
err = iter.ForEach(func(ref *plumbing.Reference) error {
100+
err := walkfn(string(ref.Name()))
101+
i++
102+
return err
103+
})
104+
return i, err
105+
}

modules/git/repo_branch_nogogit.go

+36-14
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,29 @@ func (repo *Repository) IsBranchExist(name string) bool {
6161
return repo.IsReferenceExist(BranchPrefix + name)
6262
}
6363

64-
// GetBranches returns branches from the repository, skipping skip initial branches and
64+
// GetBranchNames returns branches from the repository, skipping skip initial branches and
6565
// returning at most limit branches, or all branches if limit is 0.
66-
func (repo *Repository) GetBranches(skip, limit int) ([]string, int, error) {
66+
func (repo *Repository) GetBranchNames(skip, limit int) ([]string, int, error) {
6767
return callShowRef(repo.Ctx, repo.Path, BranchPrefix, "--heads", skip, limit)
6868
}
6969

70+
// WalkReferences walks all the references from the repository
71+
func WalkReferences(ctx context.Context, repoPath string, walkfn func(string) error) (int, error) {
72+
return walkShowRef(ctx, repoPath, "", 0, 0, walkfn)
73+
}
74+
7075
// callShowRef return refs, if limit = 0 it will not limit
7176
func callShowRef(ctx context.Context, repoPath, prefix, arg string, skip, limit int) (branchNames []string, countAll int, err error) {
77+
countAll, err = walkShowRef(ctx, repoPath, arg, skip, limit, func(branchName string) error {
78+
branchName = strings.TrimPrefix(branchName, prefix)
79+
branchNames = append(branchNames, branchName)
80+
81+
return nil
82+
})
83+
return
84+
}
85+
86+
func walkShowRef(ctx context.Context, repoPath, arg string, skip, limit int, walkfn func(string) error) (countAll int, err error) {
7287
stdoutReader, stdoutWriter := io.Pipe()
7388
defer func() {
7489
_ = stdoutReader.Close()
@@ -77,7 +92,11 @@ func callShowRef(ctx context.Context, repoPath, prefix, arg string, skip, limit
7792

7893
go func() {
7994
stderrBuilder := &strings.Builder{}
80-
err := NewCommandContext(ctx, "show-ref", arg).RunInDirPipeline(repoPath, stdoutWriter, stderrBuilder)
95+
args := []string{"show-ref"}
96+
if arg != "" {
97+
args = append(args, arg)
98+
}
99+
err := NewCommandContext(ctx, args...).RunInDirPipeline(repoPath, stdoutWriter, stderrBuilder)
81100
if err != nil {
82101
if stderrBuilder.Len() == 0 {
83102
_ = stdoutWriter.Close()
@@ -94,10 +113,10 @@ func callShowRef(ctx context.Context, repoPath, prefix, arg string, skip, limit
94113
for i < skip {
95114
_, isPrefix, err := bufReader.ReadLine()
96115
if err == io.EOF {
97-
return branchNames, i, nil
116+
return i, nil
98117
}
99118
if err != nil {
100-
return nil, 0, err
119+
return 0, err
101120
}
102121
if !isPrefix {
103122
i++
@@ -112,39 +131,42 @@ func callShowRef(ctx context.Context, repoPath, prefix, arg string, skip, limit
112131
_, err = bufReader.ReadSlice(' ')
113132
}
114133
if err == io.EOF {
115-
return branchNames, i, nil
134+
return i, nil
116135
}
117136
if err != nil {
118-
return nil, 0, err
137+
return 0, err
119138
}
120139

121140
branchName, err := bufReader.ReadString('\n')
122141
if err == io.EOF {
123142
// This shouldn't happen... but we'll tolerate it for the sake of peace
124-
return branchNames, i, nil
143+
return i, nil
125144
}
126145
if err != nil {
127-
return nil, i, err
146+
return i, err
128147
}
129-
branchName = strings.TrimPrefix(branchName, prefix)
148+
130149
if len(branchName) > 0 {
131150
branchName = branchName[:len(branchName)-1]
132151
}
133-
branchNames = append(branchNames, branchName)
152+
err = walkfn(branchName)
153+
if err != nil {
154+
return i, err
155+
}
134156
i++
135157
}
136158
// count all refs
137159
for limit != 0 {
138160
_, isPrefix, err := bufReader.ReadLine()
139161
if err == io.EOF {
140-
return branchNames, i, nil
162+
return i, nil
141163
}
142164
if err != nil {
143-
return nil, 0, err
165+
return 0, err
144166
}
145167
if !isPrefix {
146168
i++
147169
}
148170
}
149-
return branchNames, i, nil
171+
return i, nil
150172
}

modules/git/repo_branch_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,21 @@ func TestRepository_GetBranches(t *testing.T) {
1717
assert.NoError(t, err)
1818
defer bareRepo1.Close()
1919

20-
branches, countAll, err := bareRepo1.GetBranches(0, 2)
20+
branches, countAll, err := bareRepo1.GetBranchNames(0, 2)
2121

2222
assert.NoError(t, err)
2323
assert.Len(t, branches, 2)
2424
assert.EqualValues(t, 3, countAll)
2525
assert.ElementsMatch(t, []string{"branch1", "branch2"}, branches)
2626

27-
branches, countAll, err = bareRepo1.GetBranches(0, 0)
27+
branches, countAll, err = bareRepo1.GetBranchNames(0, 0)
2828

2929
assert.NoError(t, err)
3030
assert.Len(t, branches, 3)
3131
assert.EqualValues(t, 3, countAll)
3232
assert.ElementsMatch(t, []string{"branch1", "branch2", "master"}, branches)
3333

34-
branches, countAll, err = bareRepo1.GetBranches(5, 1)
34+
branches, countAll, err = bareRepo1.GetBranchNames(5, 1)
3535

3636
assert.NoError(t, err)
3737
assert.Len(t, branches, 0)
@@ -48,7 +48,7 @@ func BenchmarkRepository_GetBranches(b *testing.B) {
4848
defer bareRepo1.Close()
4949

5050
for i := 0; i < b.N; i++ {
51-
_, _, err := bareRepo1.GetBranches(0, 0)
51+
_, _, err := bareRepo1.GetBranchNames(0, 0)
5252
if err != nil {
5353
b.Fatal(err)
5454
}

routers/web/repo/branch.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -165,14 +165,14 @@ func redirect(ctx *context.Context) {
165165
// loadBranches loads branches from the repository limited by page & pageSize.
166166
// NOTE: May write to context on error.
167167
func loadBranches(ctx *context.Context, skip, limit int) ([]*Branch, int) {
168-
defaultBranch, err := repo_service.GetBranch(ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch)
168+
defaultBranch, err := ctx.Repo.GitRepo.GetBranch(ctx.Repo.Repository.DefaultBranch)
169169
if err != nil {
170170
log.Error("loadBranches: get default branch: %v", err)
171171
ctx.ServerError("GetDefaultBranch", err)
172172
return nil, 0
173173
}
174174

175-
rawBranches, totalNumOfBranches, err := repo_service.GetBranches(ctx.Repo.Repository, skip, limit)
175+
rawBranches, totalNumOfBranches, err := ctx.Repo.GitRepo.GetBranches(skip, limit)
176176
if err != nil {
177177
log.Error("GetBranches: %v", err)
178178
ctx.ServerError("GetBranches", err)

routers/web/repo/compare.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,7 @@ func getBranchesAndTagsForRepo(repo *models.Repository) (branches, tags []string
660660
}
661661
defer gitRepo.Close()
662662

663-
branches, _, err = gitRepo.GetBranches(0, 0)
663+
branches, _, err = gitRepo.GetBranchNames(0, 0)
664664
if err != nil {
665665
return nil, nil, err
666666
}
@@ -711,7 +711,7 @@ func CompareDiff(ctx *context.Context) {
711711
return
712712
}
713713

714-
headBranches, _, err := ci.HeadGitRepo.GetBranches(0, 0)
714+
headBranches, _, err := ci.HeadGitRepo.GetBranchNames(0, 0)
715715
if err != nil {
716716
ctx.ServerError("GetBranches", err)
717717
return

routers/web/repo/issue.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,7 @@ func RetrieveRepoMetas(ctx *context.Context, repo *models.Repository, isPull boo
690690
return nil
691691
}
692692

693-
brs, _, err := ctx.Repo.GitRepo.GetBranches(0, 0)
693+
brs, _, err := ctx.Repo.GitRepo.GetBranchNames(0, 0)
694694
if err != nil {
695695
ctx.ServerError("GetBranches", err)
696696
return nil

services/repository/adopt.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r
142142

143143
repo.DefaultBranch = strings.TrimPrefix(repo.DefaultBranch, git.BranchPrefix)
144144
}
145-
branches, _, _ := gitRepo.GetBranches(0, 0)
145+
branches, _, _ := gitRepo.GetBranchNames(0, 0)
146146
found := false
147147
hasDefault := false
148148
hasMaster := false

services/repository/branch.go

+25-28
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
package repository
66

77
import (
8+
"context"
89
"errors"
910
"fmt"
11+
"strings"
1012

1113
"code.gitea.io/gitea/models"
1214
user_model "code.gitea.io/gitea/models/user"
@@ -20,7 +22,7 @@ import (
2022
// CreateNewBranch creates a new repository branch
2123
func CreateNewBranch(doer *user_model.User, repo *models.Repository, oldBranchName, branchName string) (err error) {
2224
// Check if branch name can be used
23-
if err := checkBranchName(repo, branchName); err != nil {
25+
if err := checkBranchName(git.DefaultContext, repo, branchName); err != nil {
2426
return err
2527
}
2628

@@ -65,44 +67,39 @@ func GetBranches(repo *models.Repository, skip, limit int) ([]*git.Branch, int,
6567
}
6668

6769
// checkBranchName validates branch name with existing repository branches
68-
func checkBranchName(repo *models.Repository, name string) error {
69-
gitRepo, err := git.OpenRepository(repo.RepoPath())
70-
if err != nil {
71-
return err
72-
}
73-
defer gitRepo.Close()
74-
75-
branches, _, err := GetBranches(repo, 0, 0)
76-
if err != nil {
77-
return err
78-
}
79-
80-
for _, branch := range branches {
81-
if branch.Name == name {
70+
func checkBranchName(ctx context.Context, repo *models.Repository, name string) error {
71+
_, err := git.WalkReferences(ctx, repo.RepoPath(), func(refName string) error {
72+
branchRefName := strings.TrimPrefix(refName, git.BranchPrefix)
73+
switch {
74+
case branchRefName == name:
8275
return models.ErrBranchAlreadyExists{
83-
BranchName: branch.Name,
76+
BranchName: name,
8477
}
85-
} else if (len(branch.Name) < len(name) && branch.Name+"/" == name[0:len(branch.Name)+1]) ||
86-
(len(branch.Name) > len(name) && name+"/" == branch.Name[0:len(name)+1]) {
78+
// If branchRefName like a/b but we want to create a branch named a then we have a conflict
79+
case strings.HasPrefix(branchRefName, name+"/"):
8780
return models.ErrBranchNameConflict{
88-
BranchName: branch.Name,
81+
BranchName: branchRefName,
82+
}
83+
// Conversely if branchRefName like a but we want to create a branch named a/b then we also have a conflict
84+
case strings.HasPrefix(name, branchRefName+"/"):
85+
return models.ErrBranchNameConflict{
86+
BranchName: branchRefName,
87+
}
88+
case refName == git.TagPrefix+name:
89+
return models.ErrTagAlreadyExists{
90+
TagName: name,
8991
}
9092
}
91-
}
92-
93-
if _, err := gitRepo.GetTag(name); err == nil {
94-
return models.ErrTagAlreadyExists{
95-
TagName: name,
96-
}
97-
}
93+
return nil
94+
})
9895

99-
return nil
96+
return err
10097
}
10198

10299
// CreateNewBranchFromCommit creates a new repository branch
103100
func CreateNewBranchFromCommit(doer *user_model.User, repo *models.Repository, commit, branchName string) (err error) {
104101
// Check if branch name can be used
105-
if err := checkBranchName(repo, branchName); err != nil {
102+
if err := checkBranchName(git.DefaultContext, repo, branchName); err != nil {
106103
return err
107104
}
108105

0 commit comments

Comments
 (0)