mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08: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:
		
							
								
								
									
										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) {
 | 
			
		||||
                if ($choice.data('url')) {
 | 
			
		||||
                    window.location.href = $choice.data('url');
 | 
			
		||||
                console.log($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>
 | 
			
		||||
			<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>
 | 
			
		||||
			<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}}
 | 
			
		||||
					</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>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user