mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-08-30 14:26:04 +02: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
14 changed files with 641 additions and 69 deletions
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…
Reference in a new issue