mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 18:12:20 +01:00 
			
		
		
		
	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
This commit is contained in:
		
							parent
							
								
									c25303b11c
								
							
						
					
					
						commit
						f3833b7ce4
					
				
							
								
								
									
										132
									
								
								integrations/repo_branch_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								integrations/repo_branch_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,132 @@ | ||||
| // Copyright 2017 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" | ||||
| 	"path" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/Unknwon/i18n" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| func testCreateBranch(t *testing.T, session *TestSession, user, repo, oldRefName, newBranchName string, expectedStatus int) string { | ||||
| 	var csrf string | ||||
| 	if expectedStatus == http.StatusNotFound { | ||||
| 		csrf = GetCSRF(t, session, path.Join(user, repo, "src/master")) | ||||
| 	} else { | ||||
| 		csrf = GetCSRF(t, session, path.Join(user, repo, "src", oldRefName)) | ||||
| 	} | ||||
| 	req := NewRequestWithValues(t, "POST", path.Join(user, repo, "branches/_new", oldRefName), map[string]string{ | ||||
| 		"_csrf":           csrf, | ||||
| 		"new_branch_name": newBranchName, | ||||
| 	}) | ||||
| 	resp := session.MakeRequest(t, req, expectedStatus) | ||||
| 	if expectedStatus != http.StatusFound { | ||||
| 		return "" | ||||
| 	} | ||||
| 	return RedirectURL(t, resp) | ||||
| } | ||||
| 
 | ||||
| func TestCreateBranch(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		OldBranchOrCommit string | ||||
| 		NewBranch         string | ||||
| 		CreateRelease     string | ||||
| 		FlashMessage      string | ||||
| 		ExpectedStatus    int | ||||
| 	}{ | ||||
| 		{ | ||||
| 			OldBranchOrCommit: "master", | ||||
| 			NewBranch:         "feature/test1", | ||||
| 			ExpectedStatus:    http.StatusFound, | ||||
| 			FlashMessage:      i18n.Tr("en", "repo.branch.create_success", "feature/test1"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			OldBranchOrCommit: "master", | ||||
| 			NewBranch:         "", | ||||
| 			ExpectedStatus:    http.StatusFound, | ||||
| 			FlashMessage:      i18n.Tr("en", "form.NewBranchName") + i18n.Tr("en", "form.require_error"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			OldBranchOrCommit: "master", | ||||
| 			NewBranch:         "feature=test1", | ||||
| 			ExpectedStatus:    http.StatusFound, | ||||
| 			FlashMessage:      i18n.Tr("en", "form.NewBranchName") + i18n.Tr("en", "form.git_ref_name_error"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			OldBranchOrCommit: "master", | ||||
| 			NewBranch:         strings.Repeat("b", 101), | ||||
| 			ExpectedStatus:    http.StatusFound, | ||||
| 			FlashMessage:      i18n.Tr("en", "form.NewBranchName") + i18n.Tr("en", "form.max_size_error", "100"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			OldBranchOrCommit: "master", | ||||
| 			NewBranch:         "master", | ||||
| 			ExpectedStatus:    http.StatusFound, | ||||
| 			FlashMessage:      i18n.Tr("en", "repo.branch.branch_already_exists", "master"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			OldBranchOrCommit: "master", | ||||
| 			NewBranch:         "master/test", | ||||
| 			ExpectedStatus:    http.StatusFound, | ||||
| 			FlashMessage:      i18n.Tr("en", "repo.branch.branch_name_conflict", "master/test", "master"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			OldBranchOrCommit: "acd1d892867872cb47f3993468605b8aa59aa2e0", | ||||
| 			NewBranch:         "feature/test2", | ||||
| 			ExpectedStatus:    http.StatusNotFound, | ||||
| 		}, | ||||
| 		{ | ||||
| 			OldBranchOrCommit: "65f1bf27bc3bf70f64657658635e66094edbcb4d", | ||||
| 			NewBranch:         "feature/test3", | ||||
| 			ExpectedStatus:    http.StatusFound, | ||||
| 			FlashMessage:      i18n.Tr("en", "repo.branch.create_success", "feature/test3"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			OldBranchOrCommit: "master", | ||||
| 			NewBranch:         "v1.0.0", | ||||
| 			CreateRelease:     "v1.0.0", | ||||
| 			ExpectedStatus:    http.StatusFound, | ||||
| 			FlashMessage:      i18n.Tr("en", "repo.branch.tag_collision", "v1.0.0"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			OldBranchOrCommit: "v1.0.0", | ||||
| 			NewBranch:         "feature/test4", | ||||
| 			CreateRelease:     "v1.0.0", | ||||
| 			ExpectedStatus:    http.StatusFound, | ||||
| 			FlashMessage:      i18n.Tr("en", "repo.branch.create_success", "feature/test4"), | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, test := range tests { | ||||
| 		prepareTestEnv(t) | ||||
| 		session := loginUser(t, "user2") | ||||
| 		if test.CreateRelease != "" { | ||||
| 			createNewRelease(t, session, "/user2/repo1", test.CreateRelease, test.CreateRelease, false, false) | ||||
| 		} | ||||
| 		redirectURL := testCreateBranch(t, session, "user2", "repo1", test.OldBranchOrCommit, test.NewBranch, test.ExpectedStatus) | ||||
| 		if test.ExpectedStatus == http.StatusFound { | ||||
| 			req := NewRequest(t, "GET", redirectURL) | ||||
| 			resp := session.MakeRequest(t, req, http.StatusOK) | ||||
| 			htmlDoc := NewHTMLParser(t, resp.Body) | ||||
| 			assert.Equal(t, | ||||
| 				test.FlashMessage, | ||||
| 				strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()), | ||||
| 			) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestCreateBranchInvalidCSRF(t *testing.T) { | ||||
| 	prepareTestEnv(t) | ||||
| 	session := loginUser(t, "user2") | ||||
| 	req := NewRequestWithValues(t, "POST", "user2/repo1/branches/_new/master", map[string]string{ | ||||
| 		"_csrf":           "fake_csrf", | ||||
| 		"new_branch_name": "test", | ||||
| 	}) | ||||
| 	session.MakeRequest(t, req, http.StatusBadRequest) | ||||
| } | ||||
| @ -649,6 +649,51 @@ func (err ErrBranchNotExist) Error() string { | ||||
| 	return fmt.Sprintf("branch does not exist [name: %s]", err.Name) | ||||
| } | ||||
| 
 | ||||
| // ErrBranchAlreadyExists represents an error that branch with such name already exists | ||||
| type ErrBranchAlreadyExists struct { | ||||
| 	BranchName string | ||||
| } | ||||
| 
 | ||||
| // IsErrBranchAlreadyExists checks if an error is an ErrBranchAlreadyExists. | ||||
| func IsErrBranchAlreadyExists(err error) bool { | ||||
| 	_, ok := err.(ErrBranchAlreadyExists) | ||||
| 	return ok | ||||
| } | ||||
| 
 | ||||
| func (err ErrBranchAlreadyExists) Error() string { | ||||
| 	return fmt.Sprintf("branch already exists [name: %s]", err.BranchName) | ||||
| } | ||||
| 
 | ||||
| // ErrBranchNameConflict represents an error that branch name conflicts with other branch | ||||
| type ErrBranchNameConflict struct { | ||||
| 	BranchName string | ||||
| } | ||||
| 
 | ||||
| // IsErrBranchNameConflict checks if an error is an ErrBranchNameConflict. | ||||
| func IsErrBranchNameConflict(err error) bool { | ||||
| 	_, ok := err.(ErrBranchNameConflict) | ||||
| 	return ok | ||||
| } | ||||
| 
 | ||||
| func (err ErrBranchNameConflict) Error() string { | ||||
| 	return fmt.Sprintf("branch conflicts with existing branch [name: %s]", err.BranchName) | ||||
| } | ||||
| 
 | ||||
| // ErrTagAlreadyExists represents an error that tag with such name already exists | ||||
| type ErrTagAlreadyExists struct { | ||||
| 	TagName string | ||||
| } | ||||
| 
 | ||||
| // IsErrTagAlreadyExists checks if an error is an ErrTagAlreadyExists. | ||||
| func IsErrTagAlreadyExists(err error) bool { | ||||
| 	_, ok := err.(ErrTagAlreadyExists) | ||||
| 	return ok | ||||
| } | ||||
| 
 | ||||
| func (err ErrTagAlreadyExists) Error() string { | ||||
| 	return fmt.Sprintf("tag already exists [name: %s]", err.TagName) | ||||
| } | ||||
| 
 | ||||
| //  __      __      ___.   .__                   __ | ||||
| // /  \    /  \ ____\_ |__ |  |__   ____   ____ |  | __ | ||||
| // \   \/\/   // __ \| __ \|  |  \ /  _ \ /  _ \|  |/ / | ||||
|  | ||||
| @ -2426,38 +2426,3 @@ func (repo *Repository) GetUserFork(userID int64) (*Repository, error) { | ||||
| 	} | ||||
| 	return &forkedRepo, nil | ||||
| } | ||||
| 
 | ||||
| // __________                             .__ | ||||
| // \______   \____________    ____   ____ |  |__ | ||||
| //  |    |  _/\_  __ \__  \  /    \_/ ___\|  |  \ | ||||
| //  |    |   \ |  | \// __ \|   |  \  \___|   Y  \ | ||||
| //  |______  / |__|  (____  /___|  /\___  >___|  / | ||||
| //         \/             \/     \/     \/     \/ | ||||
| // | ||||
| 
 | ||||
| // CreateNewBranch creates a new repository branch | ||||
| func (repo *Repository) CreateNewBranch(doer *User, oldBranchName, branchName string) (err error) { | ||||
| 	repoWorkingPool.CheckIn(com.ToStr(repo.ID)) | ||||
| 	defer repoWorkingPool.CheckOut(com.ToStr(repo.ID)) | ||||
| 
 | ||||
| 	localPath := repo.LocalCopyPath() | ||||
| 
 | ||||
| 	if err = discardLocalRepoBranchChanges(localPath, oldBranchName); err != nil { | ||||
| 		return fmt.Errorf("discardLocalRepoChanges: %v", err) | ||||
| 	} else if err = repo.UpdateLocalCopyBranch(oldBranchName); err != nil { | ||||
| 		return fmt.Errorf("UpdateLocalCopyBranch: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if err = repo.CheckoutNewBranch(oldBranchName, branchName); err != nil { | ||||
| 		return fmt.Errorf("CreateNewBranch: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if err = git.Push(localPath, git.PushOptions{ | ||||
| 		Remote: "origin", | ||||
| 		Branch: branchName, | ||||
| 	}); err != nil { | ||||
| 		return fmt.Errorf("Push: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| @ -5,7 +5,13 @@ | ||||
| package models | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"code.gitea.io/git" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 
 | ||||
| 	"github.com/Unknwon/com" | ||||
| ) | ||||
| 
 | ||||
| // Branch holds the branch information | ||||
| @ -36,6 +42,11 @@ func GetBranchesByPath(path string) ([]*Branch, error) { | ||||
| 	return branches, nil | ||||
| } | ||||
| 
 | ||||
| // CanCreateBranch returns true if repository meets the requirements for creating new branches. | ||||
| func (repo *Repository) CanCreateBranch() bool { | ||||
| 	return !repo.IsMirror | ||||
| } | ||||
| 
 | ||||
| // GetBranch returns a branch by it's name | ||||
| func (repo *Repository) GetBranch(branch string) (*Branch, error) { | ||||
| 	if !git.IsBranchExist(repo.RepoPath(), branch) { | ||||
| @ -52,6 +63,128 @@ func (repo *Repository) GetBranches() ([]*Branch, error) { | ||||
| 	return GetBranchesByPath(repo.RepoPath()) | ||||
| } | ||||
| 
 | ||||
| // CheckBranchName validates branch name with existing repository branches | ||||
| func (repo *Repository) CheckBranchName(name string) error { | ||||
| 	gitRepo, err := git.OpenRepository(repo.RepoPath()) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if _, err := gitRepo.GetTag(name); err == nil { | ||||
| 		return ErrTagAlreadyExists{name} | ||||
| 	} | ||||
| 
 | ||||
| 	branches, err := repo.GetBranches() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	for _, branch := range branches { | ||||
| 		if branch.Name == name { | ||||
| 			return ErrBranchAlreadyExists{branch.Name} | ||||
| 		} else if (len(branch.Name) < len(name) && branch.Name+"/" == name[0:len(branch.Name)+1]) || | ||||
| 			(len(branch.Name) > len(name) && name+"/" == branch.Name[0:len(name)+1]) { | ||||
| 			return ErrBranchNameConflict{branch.Name} | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // CreateNewBranch creates a new repository branch | ||||
| func (repo *Repository) CreateNewBranch(doer *User, oldBranchName, branchName string) (err error) { | ||||
| 	repoWorkingPool.CheckIn(com.ToStr(repo.ID)) | ||||
| 	defer repoWorkingPool.CheckOut(com.ToStr(repo.ID)) | ||||
| 
 | ||||
| 	// Check if branch name can be used | ||||
| 	if err := repo.CheckBranchName(branchName); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	localPath := repo.LocalCopyPath() | ||||
| 
 | ||||
| 	if err = discardLocalRepoBranchChanges(localPath, oldBranchName); err != nil { | ||||
| 		return fmt.Errorf("discardLocalRepoChanges: %v", err) | ||||
| 	} else if err = repo.UpdateLocalCopyBranch(oldBranchName); err != nil { | ||||
| 		return fmt.Errorf("UpdateLocalCopyBranch: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if err = repo.CheckoutNewBranch(oldBranchName, branchName); err != nil { | ||||
| 		return fmt.Errorf("CreateNewBranch: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if err = git.Push(localPath, git.PushOptions{ | ||||
| 		Remote: "origin", | ||||
| 		Branch: branchName, | ||||
| 	}); err != nil { | ||||
| 		return fmt.Errorf("Push: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // updateLocalCopyToCommit pulls latest changes of given commit from repoPath to localPath. | ||||
| // It creates a new clone if local copy does not exist. | ||||
| // This function checks out target commit by default, it is safe to assume subsequent | ||||
| // operations are operating against target commit when caller has confidence for no race condition. | ||||
| func updateLocalCopyToCommit(repoPath, localPath, commit string) error { | ||||
| 	if !com.IsExist(localPath) { | ||||
| 		if err := git.Clone(repoPath, localPath, git.CloneRepoOptions{ | ||||
| 			Timeout: time.Duration(setting.Git.Timeout.Clone) * time.Second, | ||||
| 		}); err != nil { | ||||
| 			return fmt.Errorf("git clone: %v", err) | ||||
| 		} | ||||
| 	} else { | ||||
| 		_, err := git.NewCommand("fetch", "origin").RunInDir(localPath) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("git fetch origin: %v", err) | ||||
| 		} | ||||
| 		if err := git.ResetHEAD(localPath, true, "HEAD"); err != nil { | ||||
| 			return fmt.Errorf("git reset --hard HEAD: %v", err) | ||||
| 		} | ||||
| 	} | ||||
| 	if err := git.Checkout(localPath, git.CheckoutOptions{ | ||||
| 		Branch: commit, | ||||
| 	}); err != nil { | ||||
| 		return fmt.Errorf("git checkout %s: %v", commit, err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // updateLocalCopyToCommit makes sure local copy of repository is at given commit. | ||||
| func (repo *Repository) updateLocalCopyToCommit(commit string) error { | ||||
| 	return updateLocalCopyToCommit(repo.RepoPath(), repo.LocalCopyPath(), commit) | ||||
| } | ||||
| 
 | ||||
| // CreateNewBranchFromCommit creates a new repository branch | ||||
| func (repo *Repository) CreateNewBranchFromCommit(doer *User, commit, branchName string) (err error) { | ||||
| 	repoWorkingPool.CheckIn(com.ToStr(repo.ID)) | ||||
| 	defer repoWorkingPool.CheckOut(com.ToStr(repo.ID)) | ||||
| 
 | ||||
| 	// Check if branch name can be used | ||||
| 	if err := repo.CheckBranchName(branchName); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	localPath := repo.LocalCopyPath() | ||||
| 
 | ||||
| 	if err = repo.updateLocalCopyToCommit(commit); err != nil { | ||||
| 		return fmt.Errorf("UpdateLocalCopyBranch: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if err = repo.CheckoutNewBranch(commit, branchName); err != nil { | ||||
| 		return fmt.Errorf("CheckoutNewBranch: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if err = git.Push(localPath, git.PushOptions{ | ||||
| 		Remote: "origin", | ||||
| 		Branch: branchName, | ||||
| 	}); err != nil { | ||||
| 		return fmt.Errorf("Push: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // GetCommit returns all the commits of a branch | ||||
| func (branch *Branch) GetCommit() (*git.Commit, error) { | ||||
| 	gitRepo, err := git.OpenRepository(branch.Path) | ||||
|  | ||||
							
								
								
									
										20
									
								
								modules/auth/repo_branch_form.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								modules/auth/repo_branch_form.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| // Copyright 2017 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 auth | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/go-macaron/binding" | ||||
| 	macaron "gopkg.in/macaron.v1" | ||||
| ) | ||||
| 
 | ||||
| // NewBranchForm form for creating a new branch | ||||
| type NewBranchForm struct { | ||||
| 	NewBranchName string `binding:"Required;MaxSize(100);GitRefName"` | ||||
| } | ||||
| 
 | ||||
| // Validate validates the fields | ||||
| func (f *NewBranchForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { | ||||
| 	return validate(errs, ctx.Data, f, ctx.Locale) | ||||
| } | ||||
| @ -76,6 +76,11 @@ func (r *Repository) CanEnableEditor() bool { | ||||
| 	return r.Repository.CanEnableEditor() && r.IsViewBranch && r.IsWriter() | ||||
| } | ||||
| 
 | ||||
| // CanCreateBranch returns true if repository is editable and user has proper access level. | ||||
| func (r *Repository) CanCreateBranch() bool { | ||||
| 	return r.Repository.CanCreateBranch() && r.IsWriter() | ||||
| } | ||||
| 
 | ||||
| // CanCommitToBranch returns true if repository is editable and user has proper access level | ||||
| //   and branch is not protected | ||||
| func (r *Repository) CanCommitToBranch(doer *models.User) (bool, error) { | ||||
| @ -528,6 +533,7 @@ func RepoRef() macaron.Handler { | ||||
| 		ctx.Data["IsViewBranch"] = ctx.Repo.IsViewBranch | ||||
| 		ctx.Data["IsViewTag"] = ctx.Repo.IsViewTag | ||||
| 		ctx.Data["IsViewCommit"] = ctx.Repo.IsViewCommit | ||||
| 		ctx.Data["CanCreateBranch"] = ctx.Repo.CanCreateBranch() | ||||
| 
 | ||||
| 		ctx.Repo.CommitsCount, err = ctx.Repo.Commit.CommitsCount() | ||||
| 		if err != nil { | ||||
|  | ||||
| @ -44,12 +44,18 @@ func addGitRefNameBindingRule() { | ||||
| 			} | ||||
| 			// Additional rules as described at https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html | ||||
| 			if strings.HasPrefix(str, "/") || strings.HasSuffix(str, "/") || | ||||
| 				strings.HasPrefix(str, ".") || strings.HasSuffix(str, ".") || | ||||
| 				strings.HasSuffix(str, ".lock") || | ||||
| 				strings.Contains(str, "..") || strings.Contains(str, "//") { | ||||
| 				strings.HasSuffix(str, ".") || strings.Contains(str, "..") || | ||||
| 				strings.Contains(str, "//") { | ||||
| 				errs.Add([]string{name}, ErrGitRefName, "GitRefName") | ||||
| 				return false, errs | ||||
| 			} | ||||
| 			parts := strings.Split(str, "/") | ||||
| 			for _, part := range parts { | ||||
| 				if strings.HasSuffix(part, ".lock") || strings.HasPrefix(part, ".") { | ||||
| 					errs.Add([]string{name}, ErrGitRefName, "GitRefName") | ||||
| 					return false, errs | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			return true, errs | ||||
| 		}, | ||||
|  | ||||
| @ -1061,6 +1061,12 @@ branch.delete_notices_2 = - This operation will permanently delete everything in | ||||
| branch.deletion_success = %s has been deleted. | ||||
| branch.deletion_failed = Failed to delete branch %s. | ||||
| branch.delete_branch_has_new_commits = %s cannot be deleted because new commits have been added after merging. | ||||
| branch.create_branch = Create branch <strong>%s</strong> | ||||
| branch.create_from = from '%s' | ||||
| branch.create_success = Branch '%s' has been created successfully! | ||||
| branch.branch_already_exists = Branch '%s' already exists in this repository. | ||||
| branch.branch_name_conflict = Branch name '%s' conflicts with already existing branch '%s'. | ||||
| branch.tag_collision = Branch '%s' can not be created as tag with same name already exists in this repository. | ||||
| 
 | ||||
| [org] | ||||
| org_name_holder = Organization Name | ||||
|  | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @ -362,9 +362,11 @@ function initRepository() { | ||||
|         var $dropdown = $(selector); | ||||
|         $dropdown.dropdown({ | ||||
|             fullTextSearch: true, | ||||
|             selectOnKeydown: false, | ||||
|             onChange: function (text, value, $choice) { | ||||
|                 window.location.href = $choice.data('url'); | ||||
|                 console.log($choice.data('url')) | ||||
|                 if ($choice.data('url')) { | ||||
|                     window.location.href = $choice.data('url'); | ||||
|                 } | ||||
|             }, | ||||
|             message: {noResults: $dropdown.data('no-results')} | ||||
|         }); | ||||
| @ -373,15 +375,7 @@ function initRepository() { | ||||
|     // File list and commits
 | ||||
|     if ($('.repository.file.list').length > 0 || | ||||
|         ('.repository.commits').length > 0) { | ||||
|         initFilterSearchDropdown('.choose.reference .dropdown'); | ||||
| 
 | ||||
|         $('.reference.column').click(function () { | ||||
|             $('.choose.reference .scrolling.menu').css('display', 'none'); | ||||
|             $('.choose.reference .text').removeClass('black'); | ||||
|             $($(this).data('target')).css('display', 'block'); | ||||
|             $(this).find('.text').addClass('black'); | ||||
|             return false; | ||||
|         }); | ||||
|         initFilterBranchTagDropdown('.choose.reference .dropdown'); | ||||
|     } | ||||
| 
 | ||||
|     // Wiki
 | ||||
| @ -1318,7 +1312,7 @@ $(document).ready(function () { | ||||
|     }); | ||||
| 
 | ||||
|     // Semantic UI modules.
 | ||||
|     $('.dropdown').dropdown(); | ||||
|     $('.dropdown:not(.custom)').dropdown(); | ||||
|     $('.jump.dropdown').dropdown({ | ||||
|         action: 'hide', | ||||
|         onShow: function () { | ||||
| @ -1780,3 +1774,190 @@ function toggleStopwatch() { | ||||
| function cancelStopwatch() { | ||||
|     $("#cancel_stopwatch_form").submit(); | ||||
| } | ||||
| 
 | ||||
| function initFilterBranchTagDropdown(selector) { | ||||
|     $(selector).each(function() { | ||||
|         var $dropdown = $(this); | ||||
|         var $data = $dropdown.find('.data'); | ||||
|         var data = { | ||||
|             items: [], | ||||
|             mode: $data.data('mode'), | ||||
|             searchTerm: '', | ||||
|             noResults: '', | ||||
|             canCreateBranch: false, | ||||
|             menuVisible: false, | ||||
|             active: 0 | ||||
|         }; | ||||
|         $data.find('.item').each(function() { | ||||
|             data.items.push({ | ||||
|                 name: $(this).text(), | ||||
|                 url: $(this).data('url'), | ||||
|                 branch: $(this).hasClass('branch'), | ||||
|                 tag: $(this).hasClass('tag'), | ||||
|                 selected: $(this).hasClass('selected') | ||||
|             }); | ||||
|         }); | ||||
|         $data.remove(); | ||||
|         new Vue({ | ||||
|             delimiters: ['${', '}'], | ||||
|             el: this, | ||||
|             data: data, | ||||
| 
 | ||||
|             beforeMount: function () { | ||||
|                 var vm = this; | ||||
| 
 | ||||
|                 this.noResults = vm.$el.getAttribute('data-no-results'); | ||||
|                 this.canCreateBranch = vm.$el.getAttribute('data-can-create-branch') === 'true'; | ||||
| 
 | ||||
|                 document.body.addEventListener('click', function(event) { | ||||
|                     if (vm.$el.contains(event.target)) { | ||||
|                         return; | ||||
|                     } | ||||
|                     if (vm.menuVisible) { | ||||
|                         Vue.set(vm, 'menuVisible', false); | ||||
|                     } | ||||
|                 }); | ||||
|             }, | ||||
| 
 | ||||
|             watch: { | ||||
|                 menuVisible: function(visible) { | ||||
|                     if (visible) { | ||||
|                         this.focusSearchField(); | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
| 
 | ||||
|             computed: { | ||||
|                 filteredItems: function() { | ||||
|                     var vm = this; | ||||
| 
 | ||||
|                     var items = vm.items.filter(function (item) { | ||||
|                         return ((vm.mode === 'branches' && item.branch) | ||||
|                                 || (vm.mode === 'tags' && item.tag)) | ||||
|                             && (!vm.searchTerm | ||||
|                                 || item.name.toLowerCase().indexOf(vm.searchTerm.toLowerCase()) >= 0); | ||||
|                     }); | ||||
| 
 | ||||
|                     vm.active = (items.length === 0 && vm.showCreateNewBranch ? 0 : -1); | ||||
| 
 | ||||
|                     return items; | ||||
|                 }, | ||||
|                 showNoResults: function() { | ||||
|                     return this.filteredItems.length === 0 | ||||
|                             && !this.showCreateNewBranch; | ||||
|                 }, | ||||
|                 showCreateNewBranch: function() { | ||||
|                     var vm = this; | ||||
|                     if (!this.canCreateBranch || !vm.searchTerm || vm.mode === 'tags') { | ||||
|                         return false; | ||||
|                     } | ||||
| 
 | ||||
|                     return vm.items.filter(function (item) { | ||||
|                         return item.name.toLowerCase() === vm.searchTerm.toLowerCase() | ||||
|                     }).length === 0; | ||||
|                 } | ||||
|             }, | ||||
| 
 | ||||
|             methods: { | ||||
|                 selectItem: function(item) { | ||||
|                     var prev = this.getSelected(); | ||||
|                     if (prev !== null) { | ||||
|                         prev.selected = false; | ||||
|                     } | ||||
|                     item.selected = true; | ||||
|                     window.location.href = item.url; | ||||
|                 }, | ||||
|                 createNewBranch: function() { | ||||
|                     if (!this.showCreateNewBranch) { | ||||
|                         return; | ||||
|                     } | ||||
|                     this.$refs.newBranchForm.submit(); | ||||
|                 }, | ||||
|                 focusSearchField: function() { | ||||
|                     var vm = this; | ||||
|                     Vue.nextTick(function() { | ||||
|                         vm.$refs.searchField.focus(); | ||||
|                     }); | ||||
|                 }, | ||||
|                 getSelected: function() { | ||||
|                     for (var i = 0, j = this.items.length; i < j; ++i) { | ||||
|                         if (this.items[i].selected) | ||||
|                             return this.items[i]; | ||||
|                     } | ||||
|                     return null; | ||||
|                 }, | ||||
|                 getSelectedIndexInFiltered() { | ||||
|                     for (var i = 0, j = this.filteredItems.length; i < j; ++i) { | ||||
|                         if (this.filteredItems[i].selected) | ||||
|                             return i; | ||||
|                     } | ||||
|                     return -1; | ||||
|                 }, | ||||
|                 scrollToActive() { | ||||
|                     var el = this.$refs['listItem' + this.active]; | ||||
|                     if (!el || el.length === 0) { | ||||
|                         return; | ||||
|                     } | ||||
|                     if (Array.isArray(el)) { | ||||
|                         el = el[0]; | ||||
|                     } | ||||
| 
 | ||||
|                     var cont = this.$refs.scrollContainer; | ||||
| 
 | ||||
|                      if (el.offsetTop < cont.scrollTop) { | ||||
|                          cont.scrollTop = el.offsetTop; | ||||
|                      } | ||||
|                      else if (el.offsetTop + el.clientHeight > cont.scrollTop + cont.clientHeight) { | ||||
|                         cont.scrollTop = el.offsetTop + el.clientHeight - cont.clientHeight; | ||||
|                     } | ||||
|                 }, | ||||
|                 keydown: function(event) { | ||||
|                     var vm = this; | ||||
|                     if (event.keyCode === 40) { | ||||
|                         // arrow down
 | ||||
|                         event.preventDefault(); | ||||
| 
 | ||||
|                         if (vm.active === -1) { | ||||
|                             vm.active = vm.getSelectedIndexInFiltered(); | ||||
|                         } | ||||
| 
 | ||||
|                         if (vm.active + (vm.showCreateNewBranch ? 0 : 1) >= vm.filteredItems.length) { | ||||
|                             return; | ||||
|                         } | ||||
|                         vm.active++; | ||||
|                         vm.scrollToActive(); | ||||
|                     } | ||||
|                     if (event.keyCode === 38) { | ||||
|                         // arrow up
 | ||||
|                         event.preventDefault(); | ||||
| 
 | ||||
|                          if (vm.active === -1) { | ||||
|                             vm.active = vm.getSelectedIndexInFiltered(); | ||||
|                         } | ||||
| 
 | ||||
|                          if (vm.active <= 0) { | ||||
|                             return; | ||||
|                         } | ||||
|                         vm.active--; | ||||
|                         vm.scrollToActive(); | ||||
|                     } | ||||
|                     if (event.keyCode == 13) { | ||||
|                         // enter
 | ||||
|                         event.preventDefault(); | ||||
| 
 | ||||
|                          if (vm.active >= vm.filteredItems.length) { | ||||
|                             vm.createNewBranch(); | ||||
|                         } else if (vm.active >= 0) { | ||||
|                             vm.selectItem(vm.filteredItems[vm.active]); | ||||
|                         } | ||||
|                     } | ||||
|                     if (event.keyCode == 27) { | ||||
|                         // escape
 | ||||
|                         event.preventDefault(); | ||||
|                         vm.menuVisible = false; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| @ -329,6 +329,10 @@ pre, code { | ||||
| 			background-color: #a1882b !important; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	.branch-tag-choice { | ||||
| 		line-height: 20px; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -5,6 +5,8 @@ | ||||
| package repo | ||||
| 
 | ||||
| import ( | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/auth" | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| ) | ||||
| @ -30,3 +32,50 @@ func Branches(ctx *context.Context) { | ||||
| 	ctx.Data["Branches"] = brs | ||||
| 	ctx.HTML(200, tplBranch) | ||||
| } | ||||
| 
 | ||||
| // CreateBranch creates new branch in repository | ||||
| func CreateBranch(ctx *context.Context, form auth.NewBranchForm) { | ||||
| 	if !ctx.Repo.CanCreateBranch() { | ||||
| 		ctx.Handle(404, "CreateBranch", nil) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if ctx.HasError() { | ||||
| 		ctx.Flash.Error(ctx.GetErrMsg()) | ||||
| 		ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchName) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	var err error | ||||
| 	if ctx.Repo.IsViewBranch { | ||||
| 		err = ctx.Repo.Repository.CreateNewBranch(ctx.User, ctx.Repo.BranchName, form.NewBranchName) | ||||
| 	} else { | ||||
| 		err = ctx.Repo.Repository.CreateNewBranchFromCommit(ctx.User, ctx.Repo.BranchName, form.NewBranchName) | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		if models.IsErrTagAlreadyExists(err) { | ||||
| 			e := err.(models.ErrTagAlreadyExists) | ||||
| 			ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName)) | ||||
| 			ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchName) | ||||
| 			return | ||||
| 		} | ||||
| 		if models.IsErrBranchAlreadyExists(err) { | ||||
| 			e := err.(models.ErrBranchAlreadyExists) | ||||
| 			ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", e.BranchName)) | ||||
| 			ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchName) | ||||
| 			return | ||||
| 		} | ||||
| 		if models.IsErrBranchNameConflict(err) { | ||||
| 			e := err.(models.ErrBranchNameConflict) | ||||
| 			ctx.Flash.Error(ctx.Tr("repo.branch.branch_name_conflict", form.NewBranchName, e.BranchName)) | ||||
| 			ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchName) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		ctx.Handle(500, "CreateNewBranch", err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.Flash.Success(ctx.Tr("repo.branch.create_success", form.NewBranchName)) | ||||
| 	ctx.Redirect(ctx.Repo.RepoLink + "/src/" + form.NewBranchName) | ||||
| } | ||||
|  | ||||
| @ -554,6 +554,10 @@ func RegisterRoutes(m *macaron.Macaron) { | ||||
| 				return | ||||
| 			} | ||||
| 		}) | ||||
| 
 | ||||
| 		m.Group("/branches", func() { | ||||
| 			m.Post("/_new/*", context.RepoRef(), bindIgnErr(auth.NewBranchForm{}), repo.CreateBranch) | ||||
| 		}, reqRepoWriter, repo.MustBeNotBare) | ||||
| 	}, reqSignIn, context.RepoAssignment(), context.UnitTypes(), context.LoadRepoUnits()) | ||||
| 
 | ||||
| 	// Releases | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <div class="fitted item choose reference"> | ||||
| 	<div class="ui floating filter dropdown" data-no-results="{{.i18n.Tr "repo.pulls.no_results"}}"> | ||||
| 			<div class="ui basic compact tiny button"> | ||||
| 	<div class="ui floating filter dropdown custom" data-can-create-branch="{{.CanCreateBranch}}" data-no-results="{{.i18n.Tr "repo.pulls.no_results"}}"> | ||||
| 		<div class="ui basic small button" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible"> | ||||
| 			<span class="text"> | ||||
| 				<i class="octicon octicon-git-branch"></i> | ||||
| 				{{if .IsViewBranch}}{{.i18n.Tr "repo.branch"}}{{else}}{{.i18n.Tr "repo.tree"}}{{end}}: | ||||
| @ -8,37 +8,58 @@ | ||||
| 			</span> | ||||
| 			<i class="dropdown icon"></i> | ||||
| 		</div> | ||||
| 		<div class="menu"> | ||||
| 		<div class="data" style="display: none" data-mode="{{if .IsViewTag}}tags{{else}}branches{{end}}"> | ||||
| 			{{range .Branches}} | ||||
| 				<div class="item branch {{if eq $.BranchName .}}selected{{end}}" data-url="{{$.RepoLink}}/{{if $.PageIsCommits}}commits{{else}}src{{end}}/{{EscapePound .}}{{if $.TreePath}}/{{EscapePound $.TreePath}}{{end}}">{{.}}</div> | ||||
| 			{{end}} | ||||
| 			{{range .Tags}} | ||||
| 				<div class="item tag {{if eq $.BranchName .}}selected{{end}}" data-url="{{$.RepoLink}}/{{if $.PageIsCommits}}commits{{else}}src{{end}}/{{EscapePound .}}{{if $.TreePath}}/{{EscapePound $.TreePath}}{{end}}">{{.}}</div> | ||||
| 			{{end}} | ||||
| 		</div> | ||||
| 		<div class="menu transition visible" v-if="menuVisible" v-cloak> | ||||
| 			<div class="ui icon search input"> | ||||
| 				<i class="filter icon"></i> | ||||
| 				<input name="search" placeholder="{{.i18n.Tr "repo.filter_branch_and_tag"}}..."> | ||||
| 				<input name="search" ref="searchField" v-model="searchTerm" @keydown="keydown($event)" placeholder="{{.i18n.Tr "repo.filter_branch_and_tag"}}..."> | ||||
| 			</div> | ||||
| 			<div class="header"> | ||||
| 			<div class="header branch-tag-choice"> | ||||
| 				<div class="ui grid"> | ||||
| 					<div class="two column row"> | ||||
| 						<a class="reference column" href="#" data-target="#branch-list"> | ||||
| 							<span class="text {{if not .IsViewTag}}black{{end}}"> | ||||
| 						<a class="reference column" href="#" @click="mode = 'branches'; focusSearchField()"> | ||||
| 							<span class="text" :class="{black: mode == 'branches'}"> | ||||
| 								<i class="octicon octicon-git-branch"></i> {{.i18n.Tr "repo.branches"}} | ||||
| 							</span> | ||||
| 						</a> | ||||
| 						<a class="reference column" href="#" data-target="#tag-list"> | ||||
| 							<span class="text {{if .IsViewTag}}black{{end}}"> | ||||
| 						<a class="reference column" href="#" @click="mode = 'tags'; focusSearchField()"> | ||||
| 							<span class="text" :class="{black: mode == 'tags'}"> | ||||
| 								<i class="reference tags icon"></i> {{.i18n.Tr "repo.tags"}} | ||||
| 							</span> | ||||
| 						</a> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div id="branch-list" class="scrolling menu" {{if .IsViewTag}}style="display: none"{{end}}> | ||||
| 				{{range .Branches}} | ||||
| 					<div class="item {{if eq $.BranchName .}}selected{{end}}" data-url="{{$.RepoLink}}/{{if $.PageIsCommits}}commits{{else}}src{{end}}/{{EscapePound .}}{{if $.TreePath}}/{{EscapePound $.TreePath}}{{end}}">{{.}}</div> | ||||
| 				{{end}} | ||||
| 			</div> | ||||
| 			<div id="tag-list" class="scrolling menu" {{if not .IsViewTag}}style="display: none"{{end}}> | ||||
| 				{{range .Tags}} | ||||
| 					<div class="item {{if eq $.BranchName .}}selected{{end}}" data-url="{{$.RepoLink}}/{{if $.PageIsCommits}}commits{{else}}src{{end}}/{{EscapePound .}}{{if $.TreePath}}/{{EscapePound $.TreePath}}{{end}}">{{.}}</div> | ||||
| 				{{end}} | ||||
| 			<div class="scrolling menu" ref="scrollContainer"> | ||||
| 				<div v-for="(item, index) in filteredItems" :key="item.name" class="item" :class="{selected: item.selected, active: active == index}" @click="selectItem(item)" :ref="'listItem' + index">${ item.name }</div> | ||||
| 				<div class="item" v-if="showCreateNewBranch" :class="{active: active == filteredItems.length}" :ref="'listItem' + filteredItems.length"> | ||||
| 					<a href="#" @click="createNewBranch()"> | ||||
| 						<div> | ||||
| 							<i class="octicon octicon-git-branch"></i> | ||||
| 							{{.i18n.Tr "repo.branch.create_branch" `${ searchTerm }` | Safe}} | ||||
| 						</div> | ||||
| 						<div class="text small"> | ||||
| 							{{if .IsViewBranch}} | ||||
| 								{{.i18n.Tr "repo.branch.create_from" .BranchName | Safe}} | ||||
| 							{{else}} | ||||
| 								{{.i18n.Tr "repo.branch.create_from" (ShortSha .BranchName) | Safe}} | ||||
| 							{{end}} | ||||
| 						</div> | ||||
| 					</a> | ||||
| 					<form ref="newBranchForm" action="{{.RepoLink}}/branches/_new/{{EscapePound .BranchName}}" method="post"> | ||||
| 						{{.CsrfTokenHtml}} | ||||
| 						<input type="hidden" name="new_branch_name" v-model="searchTerm"> | ||||
| 					</form> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="message" v-if="showNoResults">${ noResults }</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user