Create new branch from branch selection dropdown ()

* 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:
Lauris BH 2017-10-15 22:59:24 +03:00 committed by GitHub
parent c25303b11c
commit f3833b7ce4
14 changed files with 641 additions and 69 deletions

View 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)
}

View File

@ -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)
}
// __ __ ___. .__ __
// / \ / \ ____\_ |__ | |__ ____ ____ | | __
// \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ /

View File

@ -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
}

View File

@ -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)

View 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)
}

View File

@ -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 {

View File

@ -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
},

View File

@ -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

View File

@ -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;
}
}
}
});
});
}

View File

@ -329,6 +329,10 @@ pre, code {
background-color: #a1882b !important;
}
}
.branch-tag-choice {
line-height: 20px;
}
}

View File

@ -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)
}

View File

@ -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

View File

@ -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>