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 <1012112796@qq.com> Co-authored-by: delvh <dev.lh@web.de>
This commit is contained in:
		
							parent
							
								
									56d79301b9
								
							
						
					
					
						commit
						bb39359668
					
				|  | @ -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> | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue