Skip to content

Commit bb39359

Browse files
a1012112796delvh
andauthored
Add a simple way to rename branch like gh (#15870)
- Update default branch if needed - Update protected branch if needed - Update all not merged pull request base branch name - Rename git branch - Record this rename work and auto redirect for old branch on ui Signed-off-by: a1012112796 <[email protected]> Co-authored-by: delvh <[email protected]>
1 parent 56d7930 commit bb39359

File tree

14 files changed

+357
-1
lines changed

14 files changed

+357
-1
lines changed

integrations/rename_branch_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2021 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+
"testing"
10+
11+
"code.gitea.io/gitea/models"
12+
"code.gitea.io/gitea/models/db"
13+
"github.com/stretchr/testify/assert"
14+
)
15+
16+
func TestRenameBranch(t *testing.T) {
17+
// get branch setting page
18+
session := loginUser(t, "user2")
19+
req := NewRequest(t, "GET", "/user2/repo1/settings/branches")
20+
resp := session.MakeRequest(t, req, http.StatusOK)
21+
htmlDoc := NewHTMLParser(t, resp.Body)
22+
23+
postData := map[string]string{
24+
"_csrf": htmlDoc.GetCSRF(),
25+
"from": "master",
26+
"to": "main",
27+
}
28+
req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", postData)
29+
session.MakeRequest(t, req, http.StatusFound)
30+
31+
// check new branch link
32+
req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/main/README.md", postData)
33+
session.MakeRequest(t, req, http.StatusOK)
34+
35+
// check old branch link
36+
req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/master/README.md", postData)
37+
resp = session.MakeRequest(t, req, http.StatusFound)
38+
location := resp.HeaderMap.Get("Location")
39+
assert.Equal(t, "/user2/repo1/src/branch/main/README.md", location)
40+
41+
// check db
42+
repo1 := db.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
43+
assert.Equal(t, "main", repo1.DefaultBranch)
44+
}

models/branches.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ type ProtectedBranch struct {
5353
func init() {
5454
db.RegisterModel(new(ProtectedBranch))
5555
db.RegisterModel(new(DeletedBranch))
56+
db.RegisterModel(new(RenamedBranch))
5657
}
5758

5859
// IsProtected returns if the branch is protected
@@ -588,3 +589,83 @@ func RemoveOldDeletedBranches(ctx context.Context, olderThan time.Duration) {
588589
log.Error("DeletedBranchesCleanup: %v", err)
589590
}
590591
}
592+
593+
// RenamedBranch provide renamed branch log
594+
// will check it when a branch can't be found
595+
type RenamedBranch struct {
596+
ID int64 `xorm:"pk autoincr"`
597+
RepoID int64 `xorm:"INDEX NOT NULL"`
598+
From string
599+
To string
600+
CreatedUnix timeutil.TimeStamp `xorm:"created"`
601+
}
602+
603+
// FindRenamedBranch check if a branch was renamed
604+
func FindRenamedBranch(repoID int64, from string) (branch *RenamedBranch, exist bool, err error) {
605+
branch = &RenamedBranch{
606+
RepoID: repoID,
607+
From: from,
608+
}
609+
exist, err = db.GetEngine(db.DefaultContext).Get(branch)
610+
611+
return
612+
}
613+
614+
// RenameBranch rename a branch
615+
func (repo *Repository) RenameBranch(from, to string, gitAction func(isDefault bool) error) (err error) {
616+
sess := db.NewSession(db.DefaultContext)
617+
defer sess.Close()
618+
if err := sess.Begin(); err != nil {
619+
return err
620+
}
621+
622+
// 1. update default branch if needed
623+
isDefault := repo.DefaultBranch == from
624+
if isDefault {
625+
repo.DefaultBranch = to
626+
_, err = sess.ID(repo.ID).Cols("default_branch").Update(repo)
627+
if err != nil {
628+
return err
629+
}
630+
}
631+
632+
// 2. Update protected branch if needed
633+
protectedBranch, err := getProtectedBranchBy(sess, repo.ID, from)
634+
if err != nil {
635+
return err
636+
}
637+
638+
if protectedBranch != nil {
639+
protectedBranch.BranchName = to
640+
_, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch)
641+
if err != nil {
642+
return err
643+
}
644+
}
645+
646+
// 3. Update all not merged pull request base branch name
647+
_, err = sess.Table(new(PullRequest)).Where("base_repo_id=? AND base_branch=? AND has_merged=?",
648+
repo.ID, from, false).
649+
Update(map[string]interface{}{"base_branch": to})
650+
if err != nil {
651+
return err
652+
}
653+
654+
// 4. do git action
655+
if err = gitAction(isDefault); err != nil {
656+
return err
657+
}
658+
659+
// 5. insert renamed branch record
660+
renamedBranch := &RenamedBranch{
661+
RepoID: repo.ID,
662+
From: from,
663+
To: to,
664+
}
665+
_, err = sess.Insert(renamedBranch)
666+
if err != nil {
667+
return err
668+
}
669+
670+
return sess.Commit()
671+
}

models/branches_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,52 @@ func getDeletedBranch(t *testing.T, branch *DeletedBranch) *DeletedBranch {
7979

8080
return deletedBranch
8181
}
82+
83+
func TestFindRenamedBranch(t *testing.T) {
84+
assert.NoError(t, db.PrepareTestDatabase())
85+
branch, exist, err := FindRenamedBranch(1, "dev")
86+
assert.NoError(t, err)
87+
assert.Equal(t, true, exist)
88+
assert.Equal(t, "master", branch.To)
89+
90+
_, exist, err = FindRenamedBranch(1, "unknow")
91+
assert.NoError(t, err)
92+
assert.Equal(t, false, exist)
93+
}
94+
95+
func TestRenameBranch(t *testing.T) {
96+
assert.NoError(t, db.PrepareTestDatabase())
97+
repo1 := db.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
98+
_isDefault := false
99+
100+
err := UpdateProtectBranch(repo1, &ProtectedBranch{
101+
RepoID: repo1.ID,
102+
BranchName: "master",
103+
}, WhitelistOptions{})
104+
assert.NoError(t, err)
105+
106+
assert.NoError(t, repo1.RenameBranch("master", "main", func(isDefault bool) error {
107+
_isDefault = isDefault
108+
return nil
109+
}))
110+
111+
assert.Equal(t, true, _isDefault)
112+
repo1 = db.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
113+
assert.Equal(t, "main", repo1.DefaultBranch)
114+
115+
pull := db.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest) // merged
116+
assert.Equal(t, "master", pull.BaseBranch)
117+
118+
pull = db.AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest) // open
119+
assert.Equal(t, "main", pull.BaseBranch)
120+
121+
renamedBranch := db.AssertExistsAndLoadBean(t, &RenamedBranch{ID: 2}).(*RenamedBranch)
122+
assert.Equal(t, "master", renamedBranch.From)
123+
assert.Equal(t, "main", renamedBranch.To)
124+
assert.Equal(t, int64(1), renamedBranch.RepoID)
125+
126+
db.AssertExistsAndLoadBean(t, &ProtectedBranch{
127+
RepoID: repo1.ID,
128+
BranchName: "main",
129+
})
130+
}

models/fixtures/renamed_branch.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-
2+
id: 1
3+
repo_id: 1
4+
from: dev
5+
to: master

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,8 @@ var migrations = []Migration{
346346
NewMigration("Add table commit_status_index", addTableCommitStatusIndex),
347347
// v196 -> v197
348348
NewMigration("Add Color to ProjectBoard table", addColorColToProjectBoard),
349+
// v197 -> v198
350+
NewMigration("Add renamed_branch table", addRenamedBranchTable),
349351
}
350352

351353
// GetCurrentDBVersion returns the current db version

models/migrations/v197.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2021 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 migrations
6+
7+
import (
8+
"xorm.io/xorm"
9+
)
10+
11+
func addRenamedBranchTable(x *xorm.Engine) error {
12+
type RenamedBranch struct {
13+
ID int64 `xorm:"pk autoincr"`
14+
RepoID int64 `xorm:"INDEX NOT NULL"`
15+
From string
16+
To string
17+
CreatedUnix int64 `xorm:"created"`
18+
}
19+
return x.Sync2(new(RenamedBranch))
20+
}

modules/context/repo.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,28 @@ func getRefName(ctx *Context, pathType RepoRefType) string {
705705
ctx.Repo.TreePath = path
706706
return ctx.Repo.Repository.DefaultBranch
707707
case RepoRefBranch:
708-
return getRefNameFromPath(ctx, path, ctx.Repo.GitRepo.IsBranchExist)
708+
ref := getRefNameFromPath(ctx, path, ctx.Repo.GitRepo.IsBranchExist)
709+
if len(ref) == 0 {
710+
// maybe it's a renamed branch
711+
return getRefNameFromPath(ctx, path, func(s string) bool {
712+
b, exist, err := models.FindRenamedBranch(ctx.Repo.Repository.ID, s)
713+
if err != nil {
714+
log.Error("FindRenamedBranch", err)
715+
return false
716+
}
717+
718+
if !exist {
719+
return false
720+
}
721+
722+
ctx.Data["IsRenamedBranch"] = true
723+
ctx.Data["RenamedBranchName"] = b.To
724+
725+
return true
726+
})
727+
}
728+
729+
return ref
709730
case RepoRefTag:
710731
return getRefNameFromPath(ctx, path, ctx.Repo.GitRepo.IsTagExist)
711732
case RepoRefCommit:
@@ -784,6 +805,15 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
784805
} else {
785806
refName = getRefName(ctx, refType)
786807
ctx.Repo.BranchName = refName
808+
isRenamedBranch, has := ctx.Data["IsRenamedBranch"].(bool)
809+
if isRenamedBranch && has {
810+
renamedBranchName := ctx.Data["RenamedBranchName"].(string)
811+
ctx.Flash.Info(ctx.Tr("repo.branch.renamed", refName, renamedBranchName))
812+
link := strings.Replace(ctx.Req.RequestURI, refName, renamedBranchName, 1)
813+
ctx.Redirect(link)
814+
return
815+
}
816+
787817
if refType.RefTypeIncludesBranches() && ctx.Repo.GitRepo.IsBranchExist(refName) {
788818
ctx.Repo.IsViewBranch = true
789819

modules/git/repo_branch.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,9 @@ func (repo *Repository) RemoveRemote(name string) error {
164164
func (branch *Branch) GetCommit() (*Commit, error) {
165165
return branch.gitRepo.GetBranchCommit(branch.Name)
166166
}
167+
168+
// RenameBranch rename a branch
169+
func (repo *Repository) RenameBranch(from, to string) error {
170+
_, err := NewCommand("branch", "-m", from, to).RunInDir(repo.Path)
171+
return err
172+
}

options/locale/locale_en-US.ini

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1985,6 +1985,12 @@ settings.lfs_pointers.inRepo=In Repo
19851985
settings.lfs_pointers.exists=Exists in store
19861986
settings.lfs_pointers.accessible=Accessible to User
19871987
settings.lfs_pointers.associateAccessible=Associate accessible %d OIDs
1988+
settings.rename_branch_failed_exist=Cannot rename branch because target branch %s exists.
1989+
settings.rename_branch_failed_not_exist=Cannot rename branch %s because it does not exist.
1990+
settings.rename_branch_success =Branch %s was successfully renamed to %s.
1991+
settings.rename_branch_from=old branch name
1992+
settings.rename_branch_to=new branch name
1993+
settings.rename_branch=Rename branch
19881994

19891995
diff.browse_source = Browse Source
19901996
diff.parent = parent
@@ -2106,6 +2112,7 @@ branch.create_new_branch = Create branch from branch:
21062112
branch.confirm_create_branch = Create branch
21072113
branch.new_branch = Create new branch
21082114
branch.new_branch_from = Create new branch from '%s'
2115+
branch.renamed = Branch %s was renamed to %s.
21092116

21102117
tag.create_tag = Create tag <strong>%s</strong>
21112118
tag.create_success = Tag '%s' has been created.

routers/web/repo/setting_protected_branch.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"code.gitea.io/gitea/modules/web"
2020
"code.gitea.io/gitea/services/forms"
2121
pull_service "code.gitea.io/gitea/services/pull"
22+
"code.gitea.io/gitea/services/repository"
2223
)
2324

2425
// ProtectedBranch render the page to protect the repository
@@ -285,3 +286,40 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
285286
ctx.Redirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
286287
}
287288
}
289+
290+
// RenameBranchPost responses for rename a branch
291+
func RenameBranchPost(ctx *context.Context) {
292+
form := web.GetForm(ctx).(*forms.RenameBranchForm)
293+
294+
if !ctx.Repo.CanCreateBranch() {
295+
ctx.NotFound("RenameBranch", nil)
296+
return
297+
}
298+
299+
if ctx.HasError() {
300+
ctx.Flash.Error(ctx.GetErrMsg())
301+
ctx.Redirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
302+
return
303+
}
304+
305+
msg, err := repository.RenameBranch(ctx.Repo.Repository, ctx.User, ctx.Repo.GitRepo, form.From, form.To)
306+
if err != nil {
307+
ctx.ServerError("RenameBranch", err)
308+
return
309+
}
310+
311+
if msg == "target_exist" {
312+
ctx.Flash.Error(ctx.Tr("repo.settings.rename_branch_failed_exist", form.To))
313+
ctx.Redirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
314+
return
315+
}
316+
317+
if msg == "from_not_exist" {
318+
ctx.Flash.Error(ctx.Tr("repo.settings.rename_branch_failed_not_exist", form.From))
319+
ctx.Redirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
320+
return
321+
}
322+
323+
ctx.Flash.Success(ctx.Tr("repo.settings.rename_branch_success", form.From, form.To))
324+
ctx.Redirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
325+
}

routers/web/web.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,7 @@ func RegisterRoutes(m *web.Route) {
612612
m.Combo("/*").Get(repo.SettingsProtectedBranch).
613613
Post(bindIgnErr(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo.SettingsProtectedBranchPost)
614614
}, repo.MustBeNotEmpty)
615+
m.Post("/rename_branch", bindIgnErr(forms.RenameBranchForm{}), context.RepoMustNotBeArchived(), repo.RenameBranchPost)
615616

616617
m.Group("/tags", func() {
617618
m.Get("", repo.Tags)

services/forms/repo_branch_form.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,15 @@ func (f *NewBranchForm) Validate(req *http.Request, errs binding.Errors) binding
2424
ctx := context.GetContext(req)
2525
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
2626
}
27+
28+
// RenameBranchForm form for rename a branch
29+
type RenameBranchForm struct {
30+
From string `binding:"Required;MaxSize(100);GitRefName"`
31+
To string `binding:"Required;MaxSize(100);GitRefName"`
32+
}
33+
34+
// Validate validates the fields
35+
func (f *RenameBranchForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
36+
ctx := context.GetContext(req)
37+
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
38+
}

0 commit comments

Comments
 (0)