- 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: a101211279
<1012112796@qq.com>
Co-authored-by: delvh <dev.lh@web.de>
tags/v1.16.0-rc1
@@ -0,0 +1,44 @@ | |||
// Copyright 2021 The Gitea Authors. All rights reserved. | |||
// Use of this source code is governed by a MIT-style | |||
// license that can be found in the LICENSE file. | |||
package integrations | |||
import ( | |||
"net/http" | |||
"testing" | |||
"code.gitea.io/gitea/models" | |||
"code.gitea.io/gitea/models/db" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
func TestRenameBranch(t *testing.T) { | |||
// get branch setting page | |||
session := loginUser(t, "user2") | |||
req := NewRequest(t, "GET", "/user2/repo1/settings/branches") | |||
resp := session.MakeRequest(t, req, http.StatusOK) | |||
htmlDoc := NewHTMLParser(t, resp.Body) | |||
postData := map[string]string{ | |||
"_csrf": htmlDoc.GetCSRF(), | |||
"from": "master", | |||
"to": "main", | |||
} | |||
req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", postData) | |||
session.MakeRequest(t, req, http.StatusFound) | |||
// check new branch link | |||
req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/main/README.md", postData) | |||
session.MakeRequest(t, req, http.StatusOK) | |||
// check old branch link | |||
req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/master/README.md", postData) | |||
resp = session.MakeRequest(t, req, http.StatusFound) | |||
location := resp.HeaderMap.Get("Location") | |||
assert.Equal(t, "/user2/repo1/src/branch/main/README.md", location) | |||
// check db | |||
repo1 := db.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) | |||
assert.Equal(t, "main", repo1.DefaultBranch) | |||
} |
@@ -53,6 +53,7 @@ type ProtectedBranch struct { | |||
func init() { | |||
db.RegisterModel(new(ProtectedBranch)) | |||
db.RegisterModel(new(DeletedBranch)) | |||
db.RegisterModel(new(RenamedBranch)) | |||
} | |||
// IsProtected returns if the branch is protected | |||
@@ -588,3 +589,83 @@ func RemoveOldDeletedBranches(ctx context.Context, olderThan time.Duration) { | |||
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(repoID int64, from string) (branch *RenamedBranch, exist bool, err error) { | |||
branch = &RenamedBranch{ | |||
RepoID: repoID, | |||
From: from, | |||
} | |||
exist, err = db.GetEngine(db.DefaultContext).Get(branch) | |||
return | |||
} | |||
// RenameBranch rename a branch | |||
func (repo *Repository) RenameBranch(from, to string, gitAction func(isDefault bool) error) (err error) { | |||
sess := db.NewSession(db.DefaultContext) | |||
defer sess.Close() | |||
if err := sess.Begin(); err != nil { | |||
return err | |||
} | |||
// 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 := getProtectedBranchBy(sess, repo.ID, from) | |||
if err != nil { | |||
return err | |||
} | |||
if protectedBranch != nil { | |||
protectedBranch.BranchName = to | |||
_, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch) | |||
if err != nil { | |||
return err | |||
} | |||
} | |||
// 3. Update all not merged pull request base branch name | |||
_, err = sess.Table(new(PullRequest)).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 = sess.Insert(renamedBranch) | |||
if err != nil { | |||
return err | |||
} | |||
return sess.Commit() | |||
} |
@@ -79,3 +79,52 @@ func getDeletedBranch(t *testing.T, branch *DeletedBranch) *DeletedBranch { | |||
return deletedBranch | |||
} | |||
func TestFindRenamedBranch(t *testing.T) { | |||
assert.NoError(t, db.PrepareTestDatabase()) | |||
branch, exist, err := FindRenamedBranch(1, "dev") | |||
assert.NoError(t, err) | |||
assert.Equal(t, true, exist) | |||
assert.Equal(t, "master", branch.To) | |||
_, exist, err = FindRenamedBranch(1, "unknow") | |||
assert.NoError(t, err) | |||
assert.Equal(t, false, exist) | |||
} | |||
func TestRenameBranch(t *testing.T) { | |||
assert.NoError(t, db.PrepareTestDatabase()) | |||
repo1 := db.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) | |||
_isDefault := false | |||
err := UpdateProtectBranch(repo1, &ProtectedBranch{ | |||
RepoID: repo1.ID, | |||
BranchName: "master", | |||
}, WhitelistOptions{}) | |||
assert.NoError(t, err) | |||
assert.NoError(t, repo1.RenameBranch("master", "main", func(isDefault bool) error { | |||
_isDefault = isDefault | |||
return nil | |||
})) | |||
assert.Equal(t, true, _isDefault) | |||
repo1 = db.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) | |||
assert.Equal(t, "main", repo1.DefaultBranch) | |||
pull := db.AssertExistsAndLoadBean(t, &PullRequest{ID: 1}).(*PullRequest) // merged | |||
assert.Equal(t, "master", pull.BaseBranch) | |||
pull = db.AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest) // open | |||
assert.Equal(t, "main", pull.BaseBranch) | |||
renamedBranch := db.AssertExistsAndLoadBean(t, &RenamedBranch{ID: 2}).(*RenamedBranch) | |||
assert.Equal(t, "master", renamedBranch.From) | |||
assert.Equal(t, "main", renamedBranch.To) | |||
assert.Equal(t, int64(1), renamedBranch.RepoID) | |||
db.AssertExistsAndLoadBean(t, &ProtectedBranch{ | |||
RepoID: repo1.ID, | |||
BranchName: "main", | |||
}) | |||
} |
@@ -0,0 +1,5 @@ | |||
- | |||
id: 1 | |||
repo_id: 1 | |||
from: dev | |||
to: master |
@@ -346,6 +346,8 @@ var migrations = []Migration{ | |||
NewMigration("Add table commit_status_index", addTableCommitStatusIndex), | |||
// v196 -> v197 | |||
NewMigration("Add Color to ProjectBoard table", addColorColToProjectBoard), | |||
// v197 -> v198 | |||
NewMigration("Add renamed_branch table", addRenamedBranchTable), | |||
} | |||
// GetCurrentDBVersion returns the current db version |
@@ -0,0 +1,20 @@ | |||
// Copyright 2021 The Gitea Authors. All rights reserved. | |||
// Use of this source code is governed by a MIT-style | |||
// license that can be found in the LICENSE file. | |||
package migrations | |||
import ( | |||
"xorm.io/xorm" | |||
) | |||
func addRenamedBranchTable(x *xorm.Engine) error { | |||
type RenamedBranch struct { | |||
ID int64 `xorm:"pk autoincr"` | |||
RepoID int64 `xorm:"INDEX NOT NULL"` | |||
From string | |||
To string | |||
CreatedUnix int64 `xorm:"created"` | |||
} | |||
return x.Sync2(new(RenamedBranch)) | |||
} |
@@ -705,7 +705,28 @@ func getRefName(ctx *Context, pathType RepoRefType) string { | |||
ctx.Repo.TreePath = path | |||
return ctx.Repo.Repository.DefaultBranch | |||
case RepoRefBranch: | |||
return getRefNameFromPath(ctx, path, ctx.Repo.GitRepo.IsBranchExist) | |||
ref := getRefNameFromPath(ctx, path, ctx.Repo.GitRepo.IsBranchExist) | |||
if len(ref) == 0 { | |||
// maybe it's a renamed branch | |||
return getRefNameFromPath(ctx, path, func(s string) bool { | |||
b, exist, err := models.FindRenamedBranch(ctx.Repo.Repository.ID, s) | |||
if err != nil { | |||
log.Error("FindRenamedBranch", err) | |||
return false | |||
} | |||
if !exist { | |||
return false | |||
} | |||
ctx.Data["IsRenamedBranch"] = true | |||
ctx.Data["RenamedBranchName"] = b.To | |||
return true | |||
}) | |||
} | |||
return ref | |||
case RepoRefTag: | |||
return getRefNameFromPath(ctx, path, ctx.Repo.GitRepo.IsTagExist) | |||
case RepoRefCommit: | |||
@@ -784,6 +805,15 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context | |||
} else { | |||
refName = getRefName(ctx, refType) | |||
ctx.Repo.BranchName = refName | |||
isRenamedBranch, has := ctx.Data["IsRenamedBranch"].(bool) | |||
if isRenamedBranch && has { | |||
renamedBranchName := ctx.Data["RenamedBranchName"].(string) | |||
ctx.Flash.Info(ctx.Tr("repo.branch.renamed", refName, renamedBranchName)) | |||
link := strings.Replace(ctx.Req.RequestURI, refName, renamedBranchName, 1) | |||
ctx.Redirect(link) | |||
return | |||
} | |||
if refType.RefTypeIncludesBranches() && ctx.Repo.GitRepo.IsBranchExist(refName) { | |||
ctx.Repo.IsViewBranch = true | |||
@@ -164,3 +164,9 @@ func (repo *Repository) RemoveRemote(name string) error { | |||
func (branch *Branch) GetCommit() (*Commit, error) { | |||
return branch.gitRepo.GetBranchCommit(branch.Name) | |||
} | |||
// RenameBranch rename a branch | |||
func (repo *Repository) RenameBranch(from, to string) error { | |||
_, err := NewCommand("branch", "-m", from, to).RunInDir(repo.Path) | |||
return err | |||
} |
@@ -1985,6 +1985,12 @@ settings.lfs_pointers.inRepo=In Repo | |||
settings.lfs_pointers.exists=Exists in store | |||
settings.lfs_pointers.accessible=Accessible to User | |||
settings.lfs_pointers.associateAccessible=Associate accessible %d OIDs | |||
settings.rename_branch_failed_exist=Cannot rename branch because target branch %s exists. | |||
settings.rename_branch_failed_not_exist=Cannot rename branch %s because it does not exist. | |||
settings.rename_branch_success =Branch %s was successfully renamed to %s. | |||
settings.rename_branch_from=old branch name | |||
settings.rename_branch_to=new branch name | |||
settings.rename_branch=Rename branch | |||
diff.browse_source = Browse Source | |||
diff.parent = parent | |||
@@ -2106,6 +2112,7 @@ branch.create_new_branch = Create branch from branch: | |||
branch.confirm_create_branch = Create branch | |||
branch.new_branch = Create new branch | |||
branch.new_branch_from = Create new branch from '%s' | |||
branch.renamed = Branch %s was renamed to %s. | |||
tag.create_tag = Create tag <strong>%s</strong> | |||
tag.create_success = Tag '%s' has been created. |
@@ -19,6 +19,7 @@ import ( | |||
"code.gitea.io/gitea/modules/web" | |||
"code.gitea.io/gitea/services/forms" | |||
pull_service "code.gitea.io/gitea/services/pull" | |||
"code.gitea.io/gitea/services/repository" | |||
) | |||
// ProtectedBranch render the page to protect the repository | |||
@@ -285,3 +286,40 @@ func SettingsProtectedBranchPost(ctx *context.Context) { | |||
ctx.Redirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink)) | |||
} | |||
} | |||
// RenameBranchPost responses for rename a branch | |||
func RenameBranchPost(ctx *context.Context) { | |||
form := web.GetForm(ctx).(*forms.RenameBranchForm) | |||
if !ctx.Repo.CanCreateBranch() { | |||
ctx.NotFound("RenameBranch", nil) | |||
return | |||
} | |||
if ctx.HasError() { | |||
ctx.Flash.Error(ctx.GetErrMsg()) | |||
ctx.Redirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink)) | |||
return | |||
} | |||
msg, err := repository.RenameBranch(ctx.Repo.Repository, ctx.User, ctx.Repo.GitRepo, form.From, form.To) | |||
if err != nil { | |||
ctx.ServerError("RenameBranch", err) | |||
return | |||
} | |||
if msg == "target_exist" { | |||
ctx.Flash.Error(ctx.Tr("repo.settings.rename_branch_failed_exist", form.To)) | |||
ctx.Redirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink)) | |||
return | |||
} | |||
if msg == "from_not_exist" { | |||
ctx.Flash.Error(ctx.Tr("repo.settings.rename_branch_failed_not_exist", form.From)) | |||
ctx.Redirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink)) | |||
return | |||
} | |||
ctx.Flash.Success(ctx.Tr("repo.settings.rename_branch_success", form.From, form.To)) | |||
ctx.Redirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink)) | |||
} |
@@ -612,6 +612,7 @@ func RegisterRoutes(m *web.Route) { | |||
m.Combo("/*").Get(repo.SettingsProtectedBranch). | |||
Post(bindIgnErr(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo.SettingsProtectedBranchPost) | |||
}, repo.MustBeNotEmpty) | |||
m.Post("/rename_branch", bindIgnErr(forms.RenameBranchForm{}), context.RepoMustNotBeArchived(), repo.RenameBranchPost) | |||
m.Group("/tags", func() { | |||
m.Get("", repo.Tags) |
@@ -24,3 +24,15 @@ func (f *NewBranchForm) Validate(req *http.Request, errs binding.Errors) binding | |||
ctx := context.GetContext(req) | |||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale) | |||
} | |||
// RenameBranchForm form for rename a branch | |||
type RenameBranchForm struct { | |||
From string `binding:"Required;MaxSize(100);GitRefName"` | |||
To string `binding:"Required;MaxSize(100);GitRefName"` | |||
} | |||
// Validate validates the fields | |||
func (f *RenameBranchForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { | |||
ctx := context.GetContext(req) | |||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale) | |||
} |
@@ -10,10 +10,49 @@ import ( | |||
"code.gitea.io/gitea/models" | |||
"code.gitea.io/gitea/modules/git" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/notification" | |||
repo_module "code.gitea.io/gitea/modules/repository" | |||
pull_service "code.gitea.io/gitea/services/pull" | |||
) | |||
// RenameBranch rename a branch | |||
func RenameBranch(repo *models.Repository, doer *models.User, gitRepo *git.Repository, from, to string) (string, error) { | |||
if from == to { | |||
return "target_exist", nil | |||
} | |||
if gitRepo.IsBranchExist(to) { | |||
return "target_exist", nil | |||
} | |||
if !gitRepo.IsBranchExist(from) { | |||
return "from_not_exist", nil | |||
} | |||
if err := repo.RenameBranch(from, to, func(isDefault bool) error { | |||
err2 := gitRepo.RenameBranch(from, to) | |||
if err2 != nil { | |||
return err2 | |||
} | |||
if isDefault { | |||
err2 = gitRepo.SetDefaultBranch(to) | |||
if err2 != nil { | |||
return err2 | |||
} | |||
} | |||
return nil | |||
}); err != nil { | |||
return "", err | |||
} | |||
notification.NotifyDeleteRef(doer, repo, "branch", "refs/heads/"+from) | |||
notification.NotifyCreateRef(doer, repo, "branch", "refs/heads/"+to) | |||
return "", nil | |||
} | |||
// enmuerates all branch related errors | |||
var ( | |||
ErrBranchIsDefault = errors.New("branch is default") |
@@ -77,6 +77,28 @@ | |||
</div> | |||
</div> | |||
</div> | |||
{{if $.Repository.CanCreateBranch}} | |||
<h4 class="ui top attached header"> | |||
{{.i18n.Tr "repo.settings.rename_branch"}} | |||
</h4> | |||
<div class="ui attached segment"> | |||
<form class="ui form" action="{{$.Repository.Link}}/settings/rename_branch" method="post"> | |||
{{.CsrfTokenHtml}} | |||
<div class="required field"> | |||
<label for="from">{{.i18n.Tr "repo.settings.rename_branch_from"}}</label> | |||
<input id="from" name="from" required> | |||
</div> | |||
<div class="required field {{if .Err_BranchName}}error{{end}}"> | |||
<label for="to">{{.i18n.Tr "repo.settings.rename_branch_to"}}</label> | |||
<input id="to" name="to" required> | |||
</div> | |||
<div class="field"> | |||
<button class="ui green button">{{$.i18n.Tr "repo.settings.update_settings"}}</button> | |||
</div> | |||
</form> | |||
</div> | |||
{{end}} | |||
{{end}} | |||
</div> | |||
</div> |