* 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 conditiontags/v1.3.0-rc1
@@ -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) |
@@ -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 |
@@ -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> |