Skip to content

Commit f3833b7

Browse files
authored
Create new branch from branch selection dropdown (#2130)
* Create new branch from branch selection dropdown and rewrite it to VueJS * Make updateLocalCopyToCommit as not exported * Move branch name validation to model * Fix possible race condition
1 parent c25303b commit f3833b7

File tree

14 files changed

+641
-69
lines changed

14 files changed

+641
-69
lines changed

integrations/repo_branch_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright 2017 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package integrations
6+
7+
import (
8+
"net/http"
9+
"path"
10+
"strings"
11+
"testing"
12+
13+
"github.com/Unknwon/i18n"
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
func testCreateBranch(t *testing.T, session *TestSession, user, repo, oldRefName, newBranchName string, expectedStatus int) string {
18+
var csrf string
19+
if expectedStatus == http.StatusNotFound {
20+
csrf = GetCSRF(t, session, path.Join(user, repo, "src/master"))
21+
} else {
22+
csrf = GetCSRF(t, session, path.Join(user, repo, "src", oldRefName))
23+
}
24+
req := NewRequestWithValues(t, "POST", path.Join(user, repo, "branches/_new", oldRefName), map[string]string{
25+
"_csrf": csrf,
26+
"new_branch_name": newBranchName,
27+
})
28+
resp := session.MakeRequest(t, req, expectedStatus)
29+
if expectedStatus != http.StatusFound {
30+
return ""
31+
}
32+
return RedirectURL(t, resp)
33+
}
34+
35+
func TestCreateBranch(t *testing.T) {
36+
tests := []struct {
37+
OldBranchOrCommit string
38+
NewBranch string
39+
CreateRelease string
40+
FlashMessage string
41+
ExpectedStatus int
42+
}{
43+
{
44+
OldBranchOrCommit: "master",
45+
NewBranch: "feature/test1",
46+
ExpectedStatus: http.StatusFound,
47+
FlashMessage: i18n.Tr("en", "repo.branch.create_success", "feature/test1"),
48+
},
49+
{
50+
OldBranchOrCommit: "master",
51+
NewBranch: "",
52+
ExpectedStatus: http.StatusFound,
53+
FlashMessage: i18n.Tr("en", "form.NewBranchName") + i18n.Tr("en", "form.require_error"),
54+
},
55+
{
56+
OldBranchOrCommit: "master",
57+
NewBranch: "feature=test1",
58+
ExpectedStatus: http.StatusFound,
59+
FlashMessage: i18n.Tr("en", "form.NewBranchName") + i18n.Tr("en", "form.git_ref_name_error"),
60+
},
61+
{
62+
OldBranchOrCommit: "master",
63+
NewBranch: strings.Repeat("b", 101),
64+
ExpectedStatus: http.StatusFound,
65+
FlashMessage: i18n.Tr("en", "form.NewBranchName") + i18n.Tr("en", "form.max_size_error", "100"),
66+
},
67+
{
68+
OldBranchOrCommit: "master",
69+
NewBranch: "master",
70+
ExpectedStatus: http.StatusFound,
71+
FlashMessage: i18n.Tr("en", "repo.branch.branch_already_exists", "master"),
72+
},
73+
{
74+
OldBranchOrCommit: "master",
75+
NewBranch: "master/test",
76+
ExpectedStatus: http.StatusFound,
77+
FlashMessage: i18n.Tr("en", "repo.branch.branch_name_conflict", "master/test", "master"),
78+
},
79+
{
80+
OldBranchOrCommit: "acd1d892867872cb47f3993468605b8aa59aa2e0",
81+
NewBranch: "feature/test2",
82+
ExpectedStatus: http.StatusNotFound,
83+
},
84+
{
85+
OldBranchOrCommit: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
86+
NewBranch: "feature/test3",
87+
ExpectedStatus: http.StatusFound,
88+
FlashMessage: i18n.Tr("en", "repo.branch.create_success", "feature/test3"),
89+
},
90+
{
91+
OldBranchOrCommit: "master",
92+
NewBranch: "v1.0.0",
93+
CreateRelease: "v1.0.0",
94+
ExpectedStatus: http.StatusFound,
95+
FlashMessage: i18n.Tr("en", "repo.branch.tag_collision", "v1.0.0"),
96+
},
97+
{
98+
OldBranchOrCommit: "v1.0.0",
99+
NewBranch: "feature/test4",
100+
CreateRelease: "v1.0.0",
101+
ExpectedStatus: http.StatusFound,
102+
FlashMessage: i18n.Tr("en", "repo.branch.create_success", "feature/test4"),
103+
},
104+
}
105+
for _, test := range tests {
106+
prepareTestEnv(t)
107+
session := loginUser(t, "user2")
108+
if test.CreateRelease != "" {
109+
createNewRelease(t, session, "/user2/repo1", test.CreateRelease, test.CreateRelease, false, false)
110+
}
111+
redirectURL := testCreateBranch(t, session, "user2", "repo1", test.OldBranchOrCommit, test.NewBranch, test.ExpectedStatus)
112+
if test.ExpectedStatus == http.StatusFound {
113+
req := NewRequest(t, "GET", redirectURL)
114+
resp := session.MakeRequest(t, req, http.StatusOK)
115+
htmlDoc := NewHTMLParser(t, resp.Body)
116+
assert.Equal(t,
117+
test.FlashMessage,
118+
strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()),
119+
)
120+
}
121+
}
122+
}
123+
124+
func TestCreateBranchInvalidCSRF(t *testing.T) {
125+
prepareTestEnv(t)
126+
session := loginUser(t, "user2")
127+
req := NewRequestWithValues(t, "POST", "user2/repo1/branches/_new/master", map[string]string{
128+
"_csrf": "fake_csrf",
129+
"new_branch_name": "test",
130+
})
131+
session.MakeRequest(t, req, http.StatusBadRequest)
132+
}

models/error.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,51 @@ func (err ErrBranchNotExist) Error() string {
649649
return fmt.Sprintf("branch does not exist [name: %s]", err.Name)
650650
}
651651

652+
// ErrBranchAlreadyExists represents an error that branch with such name already exists
653+
type ErrBranchAlreadyExists struct {
654+
BranchName string
655+
}
656+
657+
// IsErrBranchAlreadyExists checks if an error is an ErrBranchAlreadyExists.
658+
func IsErrBranchAlreadyExists(err error) bool {
659+
_, ok := err.(ErrBranchAlreadyExists)
660+
return ok
661+
}
662+
663+
func (err ErrBranchAlreadyExists) Error() string {
664+
return fmt.Sprintf("branch already exists [name: %s]", err.BranchName)
665+
}
666+
667+
// ErrBranchNameConflict represents an error that branch name conflicts with other branch
668+
type ErrBranchNameConflict struct {
669+
BranchName string
670+
}
671+
672+
// IsErrBranchNameConflict checks if an error is an ErrBranchNameConflict.
673+
func IsErrBranchNameConflict(err error) bool {
674+
_, ok := err.(ErrBranchNameConflict)
675+
return ok
676+
}
677+
678+
func (err ErrBranchNameConflict) Error() string {
679+
return fmt.Sprintf("branch conflicts with existing branch [name: %s]", err.BranchName)
680+
}
681+
682+
// ErrTagAlreadyExists represents an error that tag with such name already exists
683+
type ErrTagAlreadyExists struct {
684+
TagName string
685+
}
686+
687+
// IsErrTagAlreadyExists checks if an error is an ErrTagAlreadyExists.
688+
func IsErrTagAlreadyExists(err error) bool {
689+
_, ok := err.(ErrTagAlreadyExists)
690+
return ok
691+
}
692+
693+
func (err ErrTagAlreadyExists) Error() string {
694+
return fmt.Sprintf("tag already exists [name: %s]", err.TagName)
695+
}
696+
652697
// __ __ ___. .__ __
653698
// / \ / \ ____\_ |__ | |__ ____ ____ | | __
654699
// \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ /

models/repo.go

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2426,38 +2426,3 @@ func (repo *Repository) GetUserFork(userID int64) (*Repository, error) {
24262426
}
24272427
return &forkedRepo, nil
24282428
}
2429-
2430-
// __________ .__
2431-
// \______ \____________ ____ ____ | |__
2432-
// | | _/\_ __ \__ \ / \_/ ___\| | \
2433-
// | | \ | | \// __ \| | \ \___| Y \
2434-
// |______ / |__| (____ /___| /\___ >___| /
2435-
// \/ \/ \/ \/ \/
2436-
//
2437-
2438-
// CreateNewBranch creates a new repository branch
2439-
func (repo *Repository) CreateNewBranch(doer *User, oldBranchName, branchName string) (err error) {
2440-
repoWorkingPool.CheckIn(com.ToStr(repo.ID))
2441-
defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
2442-
2443-
localPath := repo.LocalCopyPath()
2444-
2445-
if err = discardLocalRepoBranchChanges(localPath, oldBranchName); err != nil {
2446-
return fmt.Errorf("discardLocalRepoChanges: %v", err)
2447-
} else if err = repo.UpdateLocalCopyBranch(oldBranchName); err != nil {
2448-
return fmt.Errorf("UpdateLocalCopyBranch: %v", err)
2449-
}
2450-
2451-
if err = repo.CheckoutNewBranch(oldBranchName, branchName); err != nil {
2452-
return fmt.Errorf("CreateNewBranch: %v", err)
2453-
}
2454-
2455-
if err = git.Push(localPath, git.PushOptions{
2456-
Remote: "origin",
2457-
Branch: branchName,
2458-
}); err != nil {
2459-
return fmt.Errorf("Push: %v", err)
2460-
}
2461-
2462-
return nil
2463-
}

models/repo_branch.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@
55
package models
66

77
import (
8+
"fmt"
9+
"time"
10+
811
"code.gitea.io/git"
12+
"code.gitea.io/gitea/modules/setting"
13+
14+
"github.com/Unknwon/com"
915
)
1016

1117
// Branch holds the branch information
@@ -36,6 +42,11 @@ func GetBranchesByPath(path string) ([]*Branch, error) {
3642
return branches, nil
3743
}
3844

45+
// CanCreateBranch returns true if repository meets the requirements for creating new branches.
46+
func (repo *Repository) CanCreateBranch() bool {
47+
return !repo.IsMirror
48+
}
49+
3950
// GetBranch returns a branch by it's name
4051
func (repo *Repository) GetBranch(branch string) (*Branch, error) {
4152
if !git.IsBranchExist(repo.RepoPath(), branch) {
@@ -52,6 +63,128 @@ func (repo *Repository) GetBranches() ([]*Branch, error) {
5263
return GetBranchesByPath(repo.RepoPath())
5364
}
5465

66+
// CheckBranchName validates branch name with existing repository branches
67+
func (repo *Repository) CheckBranchName(name string) error {
68+
gitRepo, err := git.OpenRepository(repo.RepoPath())
69+
if err != nil {
70+
return err
71+
}
72+
73+
if _, err := gitRepo.GetTag(name); err == nil {
74+
return ErrTagAlreadyExists{name}
75+
}
76+
77+
branches, err := repo.GetBranches()
78+
if err != nil {
79+
return err
80+
}
81+
82+
for _, branch := range branches {
83+
if branch.Name == name {
84+
return ErrBranchAlreadyExists{branch.Name}
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]) {
87+
return ErrBranchNameConflict{branch.Name}
88+
}
89+
}
90+
return nil
91+
}
92+
93+
// CreateNewBranch creates a new repository branch
94+
func (repo *Repository) CreateNewBranch(doer *User, oldBranchName, branchName string) (err error) {
95+
repoWorkingPool.CheckIn(com.ToStr(repo.ID))
96+
defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
97+
98+
// Check if branch name can be used
99+
if err := repo.CheckBranchName(branchName); err != nil {
100+
return err
101+
}
102+
103+
localPath := repo.LocalCopyPath()
104+
105+
if err = discardLocalRepoBranchChanges(localPath, oldBranchName); err != nil {
106+
return fmt.Errorf("discardLocalRepoChanges: %v", err)
107+
} else if err = repo.UpdateLocalCopyBranch(oldBranchName); err != nil {
108+
return fmt.Errorf("UpdateLocalCopyBranch: %v", err)
109+
}
110+
111+
if err = repo.CheckoutNewBranch(oldBranchName, branchName); err != nil {
112+
return fmt.Errorf("CreateNewBranch: %v", err)
113+
}
114+
115+
if err = git.Push(localPath, git.PushOptions{
116+
Remote: "origin",
117+
Branch: branchName,
118+
}); err != nil {
119+
return fmt.Errorf("Push: %v", err)
120+
}
121+
122+
return nil
123+
}
124+
125+
// updateLocalCopyToCommit pulls latest changes of given commit from repoPath to localPath.
126+
// It creates a new clone if local copy does not exist.
127+
// This function checks out target commit by default, it is safe to assume subsequent
128+
// operations are operating against target commit when caller has confidence for no race condition.
129+
func updateLocalCopyToCommit(repoPath, localPath, commit string) error {
130+
if !com.IsExist(localPath) {
131+
if err := git.Clone(repoPath, localPath, git.CloneRepoOptions{
132+
Timeout: time.Duration(setting.Git.Timeout.Clone) * time.Second,
133+
}); err != nil {
134+
return fmt.Errorf("git clone: %v", err)
135+
}
136+
} else {
137+
_, err := git.NewCommand("fetch", "origin").RunInDir(localPath)
138+
if err != nil {
139+
return fmt.Errorf("git fetch origin: %v", err)
140+
}
141+
if err := git.ResetHEAD(localPath, true, "HEAD"); err != nil {
142+
return fmt.Errorf("git reset --hard HEAD: %v", err)
143+
}
144+
}
145+
if err := git.Checkout(localPath, git.CheckoutOptions{
146+
Branch: commit,
147+
}); err != nil {
148+
return fmt.Errorf("git checkout %s: %v", commit, err)
149+
}
150+
return nil
151+
}
152+
153+
// updateLocalCopyToCommit makes sure local copy of repository is at given commit.
154+
func (repo *Repository) updateLocalCopyToCommit(commit string) error {
155+
return updateLocalCopyToCommit(repo.RepoPath(), repo.LocalCopyPath(), commit)
156+
}
157+
158+
// CreateNewBranchFromCommit creates a new repository branch
159+
func (repo *Repository) CreateNewBranchFromCommit(doer *User, commit, branchName string) (err error) {
160+
repoWorkingPool.CheckIn(com.ToStr(repo.ID))
161+
defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
162+
163+
// Check if branch name can be used
164+
if err := repo.CheckBranchName(branchName); err != nil {
165+
return err
166+
}
167+
168+
localPath := repo.LocalCopyPath()
169+
170+
if err = repo.updateLocalCopyToCommit(commit); err != nil {
171+
return fmt.Errorf("UpdateLocalCopyBranch: %v", err)
172+
}
173+
174+
if err = repo.CheckoutNewBranch(commit, branchName); err != nil {
175+
return fmt.Errorf("CheckoutNewBranch: %v", err)
176+
}
177+
178+
if err = git.Push(localPath, git.PushOptions{
179+
Remote: "origin",
180+
Branch: branchName,
181+
}); err != nil {
182+
return fmt.Errorf("Push: %v", err)
183+
}
184+
185+
return nil
186+
}
187+
55188
// GetCommit returns all the commits of a branch
56189
func (branch *Branch) GetCommit() (*git.Commit, error) {
57190
gitRepo, err := git.OpenRepository(branch.Path)

modules/auth/repo_branch_form.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2017 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package auth
6+
7+
import (
8+
"github.com/go-macaron/binding"
9+
macaron "gopkg.in/macaron.v1"
10+
)
11+
12+
// NewBranchForm form for creating a new branch
13+
type NewBranchForm struct {
14+
NewBranchName string `binding:"Required;MaxSize(100);GitRefName"`
15+
}
16+
17+
// Validate validates the fields
18+
func (f *NewBranchForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
19+
return validate(errs, ctx.Data, f, ctx.Locale)
20+
}

0 commit comments

Comments
 (0)