]> source.dussan.org Git - gitea.git/commitdiff
Add a simple way to rename branch like gh (#15870)
authora1012112796 <1012112796@qq.com>
Fri, 8 Oct 2021 17:03:04 +0000 (01:03 +0800)
committerGitHub <noreply@github.com>
Fri, 8 Oct 2021 17:03:04 +0000 (19:03 +0200)
- 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 <1012112796@qq.com>
Co-authored-by: delvh <dev.lh@web.de>
14 files changed:
integrations/rename_branch_test.go [new file with mode: 0644]
models/branches.go
models/branches_test.go
models/fixtures/renamed_branch.yml [new file with mode: 0644]
models/migrations/migrations.go
models/migrations/v197.go [new file with mode: 0644]
modules/context/repo.go
modules/git/repo_branch.go
options/locale/locale_en-US.ini
routers/web/repo/setting_protected_branch.go
routers/web/web.go
services/forms/repo_branch_form.go
services/repository/branch.go
templates/repo/settings/branches.tmpl

diff --git a/integrations/rename_branch_test.go b/integrations/rename_branch_test.go
new file mode 100644 (file)
index 0000000..90c1f4d
--- /dev/null
@@ -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)
+}
index 8eaa4b6fd74b673eb60d69360fdedaf028430aa9..3c62c7a87bd830b4219ea9073c7d7010e1c36fd4 100644 (file)
@@ -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()
+}
index 02a9e81a7efed502fb7490a305d2e4ad1cf3bc89..f1dcfecfa8be2835bca493fd8a16c59273ea794c 100644 (file)
@@ -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",
+       })
+}
diff --git a/models/fixtures/renamed_branch.yml b/models/fixtures/renamed_branch.yml
new file mode 100644 (file)
index 0000000..efa5130
--- /dev/null
@@ -0,0 +1,5 @@
+-
+  id: 1
+  repo_id: 1
+  from: dev
+  to: master
index 33b094e48ccbbb351f774d5d2a4b48bfe7d9ebc6..6f6296dabfb92b50130d6c094fda64a89707cd26 100644 (file)
@@ -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
diff --git a/models/migrations/v197.go b/models/migrations/v197.go
new file mode 100644 (file)
index 0000000..3517896
--- /dev/null
@@ -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))
+}
index eceefd9e5925d3c7818b919f97ae91537447bbe9..8972cd28bc76321eae11cc75302a44e4ca567d00 100644 (file)
@@ -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
 
index 7c30b1fb204469c937fbb9a2d0fe57d46ad0ce5e..96f692826ec935eb912ed2bef8fb69dc51bf369f 100644 (file)
@@ -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
+}
index bc03f86619412ea3e04eab1c2d67218f03569aa1..d5af933f4041ad060a502558a09a23b9f900fa88 100644 (file)
@@ -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.
index c48ab9471a21ca2df97b3610ce3bef8b22c460b5..876ff9ba460b6deae47d0aed698fbb35ed89be64 100644 (file)
@@ -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))
+}
index 01d90d206fd3b18433b00a6aada70d077cac89a2..b4103ccad3f1429cf8eaed43c002ad23d0c02d15 100644 (file)
@@ -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)
index 88a069b8310c8d1c0b5bde2d2a128ac1cae28683..f9262aaede77a7062e8c2df0cfdc9f1a1b6fe3d2 100644 (file)
@@ -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)
+}
index 28d24f121d06bb88fd37c7cdd40f763469b9be81..5e246cbec67ca4b80d0d39b8b94b8ccea8b53310 100644 (file)
@@ -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")
index ccf6abbb81c012eaedd887948318c9caf54bcb36..89d7c6db77a31b86e6d95f30f02d16af887e964e 100644 (file)
                                        </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>