Add branch overiew page (#2108)

* Add branch overiew page

* fix changed method name on sub menu

* remove unused code
release/v1.15
Bwko 2017-10-26 02:49:16 +02:00 committed by Lunny Xiao
parent e86a0bf3fe
commit 3ab580c8d6
21 changed files with 701 additions and 52 deletions

View File

@ -0,0 +1,79 @@
// 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"
"net/url"
"testing"
"github.com/PuerkitoBio/goquery"
"github.com/Unknwon/i18n"
"github.com/stretchr/testify/assert"
)
func TestViewBranches(t *testing.T) {
prepareTestEnv(t)
req := NewRequest(t, "GET", "/user2/repo1/branches")
resp := MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
_, exists := htmlDoc.doc.Find(".delete-branch-button").Attr("data-url")
assert.False(t, exists, "The template has changed")
}
func TestDeleteBranch(t *testing.T) {
prepareTestEnv(t)
deleteBranch(t)
}
func TestUndoDeleteBranch(t *testing.T) {
prepareTestEnv(t)
deleteBranch(t)
htmlDoc, name := branchAction(t, ".undo-button")
assert.Contains(t,
htmlDoc.doc.Find(".ui.positive.message").Text(),
i18n.Tr("en", "repo.branch.restore_success", name),
)
}
func deleteBranch(t *testing.T) {
htmlDoc, name := branchAction(t, ".delete-branch-button")
assert.Contains(t,
htmlDoc.doc.Find(".ui.positive.message").Text(),
i18n.Tr("en", "repo.branch.deletion_success", name),
)
}
func branchAction(t *testing.T, button string) (*HTMLDoc, string) {
session := loginUser(t, "user2")
req := NewRequest(t, "GET", "/user2/repo1/branches")
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
link, exists := htmlDoc.doc.Find(button).Attr("data-url")
assert.True(t, exists, "The template has changed")
htmlDoc = NewHTMLParser(t, resp.Body)
req = NewRequestWithValues(t, "POST", link, map[string]string{
"_csrf": getCsrf(htmlDoc.doc),
})
resp = session.MakeRequest(t, req, http.StatusOK)
url, err := url.Parse(link)
assert.NoError(t, err)
req = NewRequest(t, "GET", "/user2/repo1/branches")
resp = session.MakeRequest(t, req, http.StatusOK)
return NewHTMLParser(t, resp.Body), url.Query()["name"][0]
}
func getCsrf(doc *goquery.Document) string {
csrf, _ := doc.Find("meta[name=\"_csrf\"]").Attr("content")
return csrf
}

View File

@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/Unknwon/com" "github.com/Unknwon/com"
@ -193,3 +194,109 @@ func (repo *Repository) DeleteProtectedBranch(id int64) (err error) {
return sess.Commit() return sess.Commit()
} }
// DeletedBranch struct
type DeletedBranch struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
Name string `xorm:"UNIQUE(s) NOT NULL"`
Commit string `xorm:"UNIQUE(s) NOT NULL"`
DeletedByID int64 `xorm:"INDEX"`
DeletedBy *User `xorm:"-"`
Deleted time.Time `xorm:"-"`
DeletedUnix int64 `xorm:"INDEX created"`
}
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
func (deletedBranch *DeletedBranch) AfterLoad() {
deletedBranch.Deleted = time.Unix(deletedBranch.DeletedUnix, 0).Local()
}
// AddDeletedBranch adds a deleted branch to the database
func (repo *Repository) AddDeletedBranch(branchName, commit string, deletedByID int64) error {
deletedBranch := &DeletedBranch{
RepoID: repo.ID,
Name: branchName,
Commit: commit,
DeletedByID: deletedByID,
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if _, err := sess.InsertOne(deletedBranch); err != nil {
return err
}
return sess.Commit()
}
// GetDeletedBranches returns all the deleted branches
func (repo *Repository) GetDeletedBranches() ([]*DeletedBranch, error) {
deletedBranches := make([]*DeletedBranch, 0)
return deletedBranches, x.Where("repo_id = ?", repo.ID).Desc("deleted_unix").Find(&deletedBranches)
}
// GetDeletedBranchByID get a deleted branch by its ID
func (repo *Repository) GetDeletedBranchByID(ID int64) (*DeletedBranch, error) {
deletedBranch := &DeletedBranch{ID: ID}
has, err := x.Get(deletedBranch)
if err != nil {
return nil, err
}
if !has {
return nil, nil
}
return deletedBranch, nil
}
// RemoveDeletedBranch removes a deleted branch from the database
func (repo *Repository) RemoveDeletedBranch(id int64) (err error) {
deletedBranch := &DeletedBranch{
RepoID: repo.ID,
ID: id,
}
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
if affected, err := sess.Delete(deletedBranch); err != nil {
return err
} else if affected != 1 {
return fmt.Errorf("remove deleted branch ID(%v) failed", id)
}
return sess.Commit()
}
// LoadUser loads the user that deleted the branch
// When there's no user found it returns a NewGhostUser
func (deletedBranch *DeletedBranch) LoadUser() {
user, err := GetUserByID(deletedBranch.DeletedByID)
if err != nil {
user = NewGhostUser()
}
deletedBranch.DeletedBy = user
}
// RemoveOldDeletedBranches removes old deleted branches
func RemoveOldDeletedBranches() {
if !taskStatusTable.StartIfNotRunning(`deleted_branches_cleanup`) {
return
}
defer taskStatusTable.Stop(`deleted_branches_cleanup`)
log.Trace("Doing: DeletedBranchesCleanup")
deleteBefore := time.Now().Add(-setting.Cron.DeletedBranchesCleanup.OlderThan)
_, err := x.Where("deleted_unix < ?", deleteBefore.Unix()).Delete(new(DeletedBranch))
if err != nil {
log.Error(4, "DeletedBranchesCleanup: %v", err)
}
}

89
models/branches_test.go Normal file
View File

@ -0,0 +1,89 @@
// 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 models
import (
"testing"
"github.com/stretchr/testify/assert"
)
var firstBranch = DeletedBranch{
ID: 1,
Name: "foo",
Commit: "1213212312313213213132131",
DeletedByID: int64(1),
}
var secondBranch = DeletedBranch{
ID: 2,
Name: "bar",
Commit: "5655464564554545466464655",
DeletedByID: int64(99),
}
func TestAddDeletedBranch(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
assert.NoError(t, repo.AddDeletedBranch(firstBranch.Name, firstBranch.Commit, firstBranch.DeletedByID))
assert.Error(t, repo.AddDeletedBranch(firstBranch.Name, firstBranch.Commit, firstBranch.DeletedByID))
assert.NoError(t, repo.AddDeletedBranch(secondBranch.Name, secondBranch.Commit, secondBranch.DeletedByID))
}
func TestGetDeletedBranches(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
AssertExistsAndLoadBean(t, &DeletedBranch{ID: 1})
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
branches, err := repo.GetDeletedBranches()
assert.NoError(t, err)
assert.Len(t, branches, 2)
}
func TestGetDeletedBranch(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
assert.NotNil(t, getDeletedBranch(t, firstBranch))
}
func TestDeletedBranchLoadUser(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
branch := getDeletedBranch(t, firstBranch)
assert.Nil(t, branch.DeletedBy)
branch.LoadUser()
assert.NotNil(t, branch.DeletedBy)
assert.Equal(t, "user1", branch.DeletedBy.Name)
branch = getDeletedBranch(t, secondBranch)
assert.Nil(t, branch.DeletedBy)
branch.LoadUser()
assert.NotNil(t, branch.DeletedBy)
assert.Equal(t, "Ghost", branch.DeletedBy.Name)
}
func TestRemoveDeletedBranch(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
branch := DeletedBranch{ID: 1}
AssertExistsAndLoadBean(t, &branch)
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
err := repo.RemoveDeletedBranch(1)
assert.NoError(t, err)
AssertNotExistsBean(t, &branch)
AssertExistsAndLoadBean(t, &DeletedBranch{ID: 2})
}
func getDeletedBranch(t *testing.T, branch DeletedBranch) *DeletedBranch {
AssertExistsAndLoadBean(t, &DeletedBranch{ID: 1})
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
deletedBranch, err := repo.GetDeletedBranchByID(branch.ID)
assert.NoError(t, err)
assert.Equal(t, branch.ID, deletedBranch.ID)
assert.Equal(t, branch.Name, deletedBranch.Name)
assert.Equal(t, branch.Commit, deletedBranch.Commit)
assert.Equal(t, branch.DeletedByID, deletedBranch.DeletedByID)
return deletedBranch
}

View File

@ -142,6 +142,8 @@ var migrations = []Migration{
NewMigration("remove index column from repo_unit table", removeIndexColumnFromRepoUnitTable), NewMigration("remove index column from repo_unit table", removeIndexColumnFromRepoUnitTable),
// v46 -> v47 // v46 -> v47
NewMigration("remove organization watch repositories", removeOrganizationWatchRepo), NewMigration("remove organization watch repositories", removeOrganizationWatchRepo),
// v47 -> v48
NewMigration("add deleted branches", addDeletedBranch),
} }
// Migrate database to current version // Migrate database to current version

29
models/migrations/v47.go Normal file
View File

@ -0,0 +1,29 @@
// 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 migrations
import (
"fmt"
"github.com/go-xorm/xorm"
)
func addDeletedBranch(x *xorm.Engine) (err error) {
// DeletedBranch contains the deleted branch information
type DeletedBranch struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
Name string `xorm:"UNIQUE(s) NOT NULL"`
Commit string `xorm:"UNIQUE(s) NOT NULL"`
DeletedByID int64 `xorm:"INDEX NOT NULL"`
DeletedUnix int64 `xorm:"INDEX"`
}
if err = x.Sync2(new(DeletedBranch)); err != nil {
return fmt.Errorf("Sync2: %v", err)
}
return nil
}

View File

@ -114,6 +114,7 @@ func init() {
new(CommitStatus), new(CommitStatus),
new(Stopwatch), new(Stopwatch),
new(TrackedTime), new(TrackedTime),
new(DeletedBranch),
) )
gonicNames := []string{"SSL", "UID"} gonicNames := []string{"SSL", "UID"}

View File

@ -77,6 +77,17 @@ func NewContext() {
go models.SyncExternalUsers() go models.SyncExternalUsers()
} }
} }
if setting.Cron.DeletedBranchesCleanup.Enabled {
entry, err = c.AddFunc("Remove old deleted branches", setting.Cron.DeletedBranchesCleanup.Schedule, models.RemoveOldDeletedBranches)
if err != nil {
log.Fatal(4, "Cron[Remove old deleted branches]: %v", err)
}
if setting.Cron.DeletedBranchesCleanup.RunAtStart {
entry.Prev = time.Now()
entry.ExecTimes++
go models.RemoveOldDeletedBranches()
}
}
c.Start() c.Start()
} }

View File

@ -365,6 +365,12 @@ var (
Schedule string Schedule string
UpdateExisting bool UpdateExisting bool
} `ini:"cron.sync_external_users"` } `ini:"cron.sync_external_users"`
DeletedBranchesCleanup struct {
Enabled bool
RunAtStart bool
Schedule string
OlderThan time.Duration
} `ini:"cron.deleted_branches_cleanup"`
}{ }{
UpdateMirror: struct { UpdateMirror: struct {
Enabled bool Enabled bool
@ -419,6 +425,17 @@ var (
Schedule: "@every 24h", Schedule: "@every 24h",
UpdateExisting: true, UpdateExisting: true,
}, },
DeletedBranchesCleanup: struct {
Enabled bool
RunAtStart bool
Schedule string
OlderThan time.Duration
}{
Enabled: true,
RunAtStart: true,
Schedule: "@every 24h",
OlderThan: 24 * time.Hour,
},
} }
// Git settings // Git settings

View File

@ -1055,10 +1055,16 @@ release.tag_name_already_exist = Release with this tag name already exists.
release.tag_name_invalid = Tag name is not valid. release.tag_name_invalid = Tag name is not valid.
release.downloads = Downloads release.downloads = Downloads
branch.name = Branch name
branch.search = Search branches
branch.already_exists = A branch named %s already exists.
branch.delete_head = Delete
branch.delete = Delete Branch %s branch.delete = Delete Branch %s
branch.delete_html = Delete Branch
branch.delete_desc = Deleting a branch is permanent. There is no way to undo it. branch.delete_desc = Deleting a branch is permanent. There is no way to undo it.
branch.delete_notices_1 = - This operation <strong>CANNOT</strong> be undone. branch.delete_notices_1 = - This operation <strong>CANNOT</strong> be undone.
branch.delete_notices_2 = - This operation will permanently delete everything in branch %s. branch.delete_notices_2 = - This operation will permanently delete everything in branch %s.
branch.delete_notices_html = - This operation will permanently delete everything in branch
branch.deletion_success = %s has been deleted. branch.deletion_success = %s has been deleted.
branch.deletion_failed = Failed to delete branch %s. 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.delete_branch_has_new_commits = %s cannot be deleted because new commits have been added after merging.
@ -1068,6 +1074,10 @@ branch.create_success = Branch '%s' has been created successfully!
branch.branch_already_exists = Branch '%s' already exists in this repository. 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.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. branch.tag_collision = Branch '%s' can not be created as tag with same name already exists in this repository.
branch.deleted_by = Deleted by %s
branch.restore_success = %s successfully restored
branch.restore_failed = Failed to restore branch %s.
branch.protected_deletion_failed = It's not possible to delete protected branch %s.
[org] [org]
org_name_holder = Organization Name org_name_holder = Organization Name

File diff suppressed because one or more lines are too long

View File

@ -1423,29 +1423,18 @@ $(document).ready(function () {
}); });
// Helpers. // Helpers.
$('.delete-button').click(function () { $('.delete-button').click(showDeletePopup);
var $this = $(this);
var filter = "";
if ($this.attr("id")) {
filter += "#"+$this.attr("id")
}
$('.delete.modal'+filter).modal({
closable: false,
onApprove: function () {
if ($this.data('type') == "form") {
$($this.data('form')).submit();
return;
}
$.post($this.data('url'), { $('.delete-branch-button').click(showDeletePopup);
"_csrf": csrf,
"id": $this.data("id") $('.undo-button').click(function() {
}).done(function (data) { var $this = $(this);
window.location.href = data.redirect; $.post($this.data('url'), {
}); "_csrf": csrf,
} "id": $this.data("id")
}).modal('show'); }).done(function(data) {
return false; window.location.href = data.redirect;
});
}); });
$('.show-panel.button').click(function () { $('.show-panel.button').click(function () {
$($(this).data('panel')).show(); $($(this).data('panel')).show();
@ -1608,6 +1597,32 @@ $(function () {
}); });
}); });
function showDeletePopup() {
var $this = $(this);
var filter = "";
if ($this.attr("id")) {
filter += "#" + $this.attr("id")
}
$('.delete.modal' + filter).modal({
closable: false,
onApprove: function() {
if ($this.data('type') == "form") {
$($this.data('form')).submit();
return;
}
$.post($this.data('url'), {
"_csrf": csrf,
"id": $this.data("id")
}).done(function(data) {
window.location.href = data.redirect;
});
}
}).modal('show');
return false;
}
function initVueComponents(){ function initVueComponents(){
var vueDelimeters = ['${', '}']; var vueDelimeters = ['${', '}'];

View File

@ -9,7 +9,7 @@
margin-bottom: 15px !important; margin-bottom: 15px !important;
background-color: #FAFAFA !important; background-color: #FAFAFA !important;
border-width: 1px !important; border-width: 1px !important;
.octicon { .octicon {
width: 16px; width: 16px;
text-align: center; text-align: center;
@ -33,7 +33,7 @@
.name { .name {
word-break: break-all; word-break: break-all;
} }
.metas { .metas {
color: #888; color: #888;
font-size: 14px; font-size: 14px;
@ -50,6 +50,13 @@
} }
} }
.ui.repository.branches {
.time{
font-size: 12px;
color: #808080;
}
}
.ui.user.list { .ui.user.list {
.item { .item {
padding-bottom: 25px; padding-bottom: 25px;

View File

@ -1313,6 +1313,27 @@
border-bottom: 1px solid #A3C293; border-bottom: 1px solid #A3C293;
} }
} }
.ui.segment.sub-menu {
padding: 7px;
line-height: 0;
.list {
width: 100%;
display: flex;
.item {
width:100%;
border-radius: 3px;
a {
color: black;
&:hover {
color: #666;
}
}
&.active {
background: rgba(0,0,0,.05);;
}
}
}
}
} }
// End of .repository // End of .repository

View File

@ -5,32 +5,192 @@
package repo package repo
import ( import (
"strings"
"code.gitea.io/git"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
) )
const ( const (
tplBranch base.TplName = "repo/branch" tplBranch base.TplName = "repo/branch/list"
) )
// Branch contains the branch information
type Branch struct {
Name string
Commit *git.Commit
IsProtected bool
IsDeleted bool
DeletedBranch *models.DeletedBranch
}
// Branches render repository branch page // Branches render repository branch page
func Branches(ctx *context.Context) { func Branches(ctx *context.Context) {
ctx.Data["Title"] = "Branches" ctx.Data["Title"] = "Branches"
ctx.Data["IsRepoToolbarBranches"] = true ctx.Data["IsRepoToolbarBranches"] = true
ctx.Data["DefaultBranch"] = ctx.Repo.Repository.DefaultBranch
ctx.Data["IsWriter"] = ctx.Repo.IsWriter()
ctx.Data["IsMirror"] = ctx.Repo.Repository.IsMirror
ctx.Data["PageIsViewCode"] = true
ctx.Data["PageIsBranches"] = true
brs, err := ctx.Repo.GitRepo.GetBranches() ctx.Data["Branches"] = loadBranches(ctx)
ctx.HTML(200, tplBranch)
}
// DeleteBranchPost responses for delete merged branch
func DeleteBranchPost(ctx *context.Context) {
defer redirect(ctx)
branchName := ctx.Query("name")
isProtected, err := ctx.Repo.Repository.IsProtectedBranch(branchName, ctx.User)
if err != nil { if err != nil {
ctx.Handle(500, "repo.Branches(GetBranches)", err) log.Error(4, "DeleteBranch: %v", err)
return ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", branchName))
} else if len(brs) == 0 {
ctx.Handle(404, "repo.Branches(GetBranches)", nil)
return return
} }
ctx.Data["Branches"] = brs if isProtected {
ctx.HTML(200, tplBranch) ctx.Flash.Error(ctx.Tr("repo.branch.protected_deletion_failed", branchName))
return
}
if !ctx.Repo.GitRepo.IsBranchExist(branchName) || branchName == ctx.Repo.Repository.DefaultBranch {
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", branchName))
return
}
if err := deleteBranch(ctx, branchName); err != nil {
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", branchName))
return
}
ctx.Flash.Success(ctx.Tr("repo.branch.deletion_success", branchName))
}
// RestoreBranchPost responses for delete merged branch
func RestoreBranchPost(ctx *context.Context) {
defer redirect(ctx)
branchID := ctx.QueryInt64("branch_id")
branchName := ctx.Query("name")
deletedBranch, err := ctx.Repo.Repository.GetDeletedBranchByID(branchID)
if err != nil {
log.Error(4, "GetDeletedBranchByID: %v", err)
ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", branchName))
return
}
if err := ctx.Repo.GitRepo.CreateBranch(deletedBranch.Name, deletedBranch.Commit); err != nil {
if strings.Contains(err.Error(), "already exists") {
ctx.Flash.Error(ctx.Tr("repo.branch.already_exists", deletedBranch.Name))
return
}
log.Error(4, "CreateBranch: %v", err)
ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", deletedBranch.Name))
return
}
if err := ctx.Repo.Repository.RemoveDeletedBranch(deletedBranch.ID); err != nil {
log.Error(4, "RemoveDeletedBranch: %v", err)
ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", deletedBranch.Name))
return
}
ctx.Flash.Success(ctx.Tr("repo.branch.restore_success", deletedBranch.Name))
}
func redirect(ctx *context.Context) {
ctx.JSON(200, map[string]interface{}{
"redirect": ctx.Repo.RepoLink + "/branches",
})
}
func deleteBranch(ctx *context.Context, branchName string) error {
commit, err := ctx.Repo.GitRepo.GetBranchCommit(branchName)
if err != nil {
log.Error(4, "GetBranchCommit: %v", err)
return err
}
if err := ctx.Repo.GitRepo.DeleteBranch(branchName, git.DeleteBranchOptions{
Force: true,
}); err != nil {
log.Error(4, "DeleteBranch: %v", err)
return err
}
// Don't return error here
if err := ctx.Repo.Repository.AddDeletedBranch(branchName, commit.ID.String(), ctx.User.ID); err != nil {
log.Warn("AddDeletedBranch: %v", err)
}
return nil
}
func loadBranches(ctx *context.Context) []*Branch {
rawBranches, err := ctx.Repo.Repository.GetBranches()
if err != nil {
ctx.Handle(500, "GetBranches", err)
return nil
}
branches := make([]*Branch, len(rawBranches))
for i := range rawBranches {
commit, err := rawBranches[i].GetCommit()
if err != nil {
ctx.Handle(500, "GetCommit", err)
return nil
}
isProtected, err := ctx.Repo.Repository.IsProtectedBranch(rawBranches[i].Name, ctx.User)
if err != nil {
ctx.Handle(500, "IsProtectedBranch", err)
return nil
}
branches[i] = &Branch{
Name: rawBranches[i].Name,
Commit: commit,
IsProtected: isProtected,
}
}
if ctx.Repo.IsWriter() {
deletedBranches, err := getDeletedBranches(ctx)
if err != nil {
ctx.Handle(500, "getDeletedBranches", err)
return nil
}
branches = append(branches, deletedBranches...)
}
return branches
}
func getDeletedBranches(ctx *context.Context) ([]*Branch, error) {
branches := []*Branch{}
deletedBranches, err := ctx.Repo.Repository.GetDeletedBranches()
if err != nil {
return branches, err
}
for i := range deletedBranches {
deletedBranches[i].LoadUser()
branches = append(branches, &Branch{
Name: deletedBranches[i].Name,
IsDeleted: true,
DeletedBranch: deletedBranches[i],
})
}
return branches, nil
} }
// CreateBranch creates new branch in repository // CreateBranch creates new branch in repository

View File

@ -53,6 +53,7 @@ func Commits(ctx *context.Context) {
ctx.Handle(404, "Commit not found", nil) ctx.Handle(404, "Commit not found", nil)
return return
} }
ctx.Data["PageIsViewCode"] = true
commitsCount, err := ctx.Repo.Commit.CommitsCount() commitsCount, err := ctx.Repo.Commit.CommitsCount()
if err != nil { if err != nil {
@ -88,6 +89,7 @@ func Commits(ctx *context.Context) {
// Graph render commit graph - show commits from all branches. // Graph render commit graph - show commits from all branches.
func Graph(ctx *context.Context) { func Graph(ctx *context.Context) {
ctx.Data["PageIsCommits"] = true ctx.Data["PageIsCommits"] = true
ctx.Data["PageIsViewCode"] = true
commitsCount, err := ctx.Repo.Commit.CommitsCount() commitsCount, err := ctx.Repo.Commit.CommitsCount()
if err != nil { if err != nil {
@ -114,6 +116,7 @@ func Graph(ctx *context.Context) {
// SearchCommits render commits filtered by keyword // SearchCommits render commits filtered by keyword
func SearchCommits(ctx *context.Context) { func SearchCommits(ctx *context.Context) {
ctx.Data["PageIsCommits"] = true ctx.Data["PageIsCommits"] = true
ctx.Data["PageIsViewCode"] = true
keyword := strings.Trim(ctx.Query("q"), " ") keyword := strings.Trim(ctx.Query("q"), " ")
if len(keyword) == 0 { if len(keyword) == 0 {

View File

@ -550,7 +550,10 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Group("/branches", func() { m.Group("/branches", func() {
m.Post("/_new/*", context.RepoRef(), bindIgnErr(auth.NewBranchForm{}), repo.CreateBranch) m.Post("/_new/*", context.RepoRef(), bindIgnErr(auth.NewBranchForm{}), repo.CreateBranch)
}, reqRepoWriter, repo.MustBeNotBare) m.Post("/delete", repo.DeleteBranchPost)
m.Post("/restore", repo.RestoreBranchPost)
}, reqRepoWriter, repo.MustBeNotBare, context.CheckUnit(models.UnitTypeCode))
}, reqSignIn, context.RepoAssignment(), context.UnitTypes(), context.LoadRepoUnits()) }, reqSignIn, context.RepoAssignment(), context.UnitTypes(), context.LoadRepoUnits())
// Releases // Releases
@ -615,6 +618,10 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("/archive/*", repo.MustBeNotBare, context.CheckUnit(models.UnitTypeCode), repo.Download) m.Get("/archive/*", repo.MustBeNotBare, context.CheckUnit(models.UnitTypeCode), repo.Download)
m.Group("/branches", func() {
m.Get("", repo.Branches)
}, repo.MustBeNotBare, context.RepoRef(), context.CheckUnit(models.UnitTypeCode))
m.Group("/pulls/:index", func() { m.Group("/pulls/:index", func() {
m.Get("/commits", context.RepoRef(), repo.ViewPullCommits) m.Get("/commits", context.RepoRef(), repo.ViewPullCommits)
m.Get("/files", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ViewPullFiles) m.Get("/files", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ViewPullFiles)

View File

@ -0,0 +1,81 @@
{{template "base/head" .}}
<div class="ui repository branches">
{{template "repo/header" .}}
<div class="ui container">
{{template "base/alert" .}}
{{template "repo/sub_menu" .}}
<h4 class="ui top attached header">
{{.i18n.Tr "repo.default_branch"}}
</h4>
<div class="ui attached table segment">
<table class="ui very basic striped fixed table single line">
<tbody>
<tr>
<td>{{.DefaultBranch}}</td>
</tr>
</tbody>
</table>
</div>
{{if gt (len .Branches) 1}}
<h4 class="ui top attached header">
{{.i18n.Tr "repo.branches"}}
</h4>
<div class="ui attached table segment">
<table class="ui very basic striped fixed table single line">
<thead>
<tr>
<th class="nine wide">{{.i18n.Tr "repo.branch.name"}}</th>
{{if and $.IsWriter (not $.IsMirror)}}
<th class="one wide right aligned">{{.i18n.Tr "repo.branch.delete_head"}}</th>
{{end}}
</tr>
</thead>
<tbody>
{{range $branch := .Branches}}
{{if ne .Name $.DefaultBranch}}
<tr>
<td>
{{if .IsDeleted}}
<s>{{.Name}}</s>
<p class="time">{{$.i18n.Tr "repo.branch.deleted_by" .DeletedBranch.DeletedBy.Name}} {{TimeSince .DeletedBranch.Deleted $.i18n.Lang}}</p>
{{else}}
{{.Name}}
<p class="time">{{$.i18n.Tr "org.repo_updated"}} {{TimeSince .Commit.Committer.When $.i18n.Lang}}</p>
</td>
{{end}}
{{if and $.IsWriter (not $.IsMirror)}}
<td class="right aligned">
{{if .IsProtected}}
<i class="octicon octicon-shield"></i>
{{else if .IsDeleted}}
<a class="undo-button" href data-url="{{$.Link}}/restore?branch_id={{.DeletedBranch.ID | urlquery}}&name={{.DeletedBranch.Name | urlquery}}"><i class="octicon octicon-reply"></i></a>
{{else}}
<a class="delete-branch-button" href data-url="{{$.Link}}/delete?name={{.Name | urlquery}}" data-val="{{.Name}}"><i class="trash icon text red"></i></a>
{{end}}
</td>
{{end}}
</tr>
{{end}}
{{end}}
</tbody>
</table>
</div>
{{end}}
</div>
</div>
<div class="ui small basic delete modal">
<div class="ui icon header">
<i class="trash icon"></i>
{{.i18n.Tr "repo.branch.delete_html"| Safe}} <span class="branch-name"></span>
</div>
<div class="content">
<p>{{.i18n.Tr "repo.branch.delete_desc" | Safe}}</p>
{{.i18n.Tr "repo.branch.delete_notices_1" | Safe}}<br>
{{.i18n.Tr "repo.branch.delete_notices_html" | Safe}} <span class="branch-name"></span><br>
</div>
{{template "base/delete_modal_actions" .}}
</div>
{{template "base/footer" .}}

View File

@ -2,18 +2,19 @@
<div class="repository commits"> <div class="repository commits">
{{template "repo/header" .}} {{template "repo/header" .}}
<div class="ui container"> <div class="ui container">
<div class="ui secondary menu"> {{template "repo/sub_menu" .}}
{{template "repo/branch_dropdown" .}} <div class="ui secondary menu">
<div class="fitted item"> {{template "repo/branch_dropdown" .}}
<a href="{{.RepoLink}}/graph" class="ui basic small button"> <div class="fitted item">
<span class="text"> <a href="{{.RepoLink}}/graph" class="ui basic small button">
<i class="octicon octicon-git-branch"></i> <span class="text">
</span> <i class="octicon octicon-git-branch"></i>
{{.i18n.Tr "repo.commit_graph"}} </span>
</a> {{.i18n.Tr "repo.commit_graph"}}
</div> </a>
</div> </div>
{{template "repo/commits_table" .}} </div>
{{template "repo/commits_table" .}}
</div> </div>
</div> </div>
{{template "base/footer" .}} {{template "base/footer" .}}

View File

@ -73,12 +73,6 @@
</a> </a>
{{end}} {{end}}
{{if and (.Repository.UnitEnabled $.UnitTypeCode) (not .IsBareRepo)}}
<a class="{{if (or (.PageIsCommits) (.PageIsDiff))}}active{{end}} item" href="{{.RepoLink}}/commits/{{EscapePound .BranchName}}">
<i class="octicon octicon-history"></i> {{.i18n.Tr "repo.commits"}} <span class="ui {{if not .CommitsCount}}gray{{else}}blue{{end}} small label">{{.CommitsCount}}</span>
</a>
{{end}}
{{if and (.Repository.UnitEnabled $.UnitTypeReleases) (not .IsBareRepo) }} {{if and (.Repository.UnitEnabled $.UnitTypeReleases) (not .IsBareRepo) }}
<a class="{{if .PageIsReleaseList}}active{{end}} item" href="{{.RepoLink}}/releases"> <a class="{{if .PageIsReleaseList}}active{{end}} item" href="{{.RepoLink}}/releases">
<i class="octicon octicon-tag"></i> {{.i18n.Tr "repo.releases"}} <span class="ui {{if not .Repository.NumReleases}}gray{{else}}blue{{end}} small label">{{.Repository.NumReleases}}</span> <i class="octicon octicon-tag"></i> {{.i18n.Tr "repo.releases"}} <span class="ui {{if not .Repository.NumReleases}}gray{{else}}blue{{end}} small label">{{.Repository.NumReleases}}</span>

View File

@ -7,6 +7,7 @@
{{if .Repository.DescriptionHTML}}<span class="description has-emoji">{{.Repository.DescriptionHTML}}</span>{{else if .IsRepositoryAdmin}}<span class="no-description text-italic">{{.i18n.Tr "repo.no_desc"}}</span>{{end}} {{if .Repository.DescriptionHTML}}<span class="description has-emoji">{{.Repository.DescriptionHTML}}</span>{{else if .IsRepositoryAdmin}}<span class="no-description text-italic">{{.i18n.Tr "repo.no_desc"}}</span>{{end}}
<a class="link" href="{{.Repository.Website}}">{{.Repository.Website}}</a> <a class="link" href="{{.Repository.Website}}">{{.Repository.Website}}</a>
</p> </p>
{{template "repo/sub_menu" .}}
<div class="ui secondary menu"> <div class="ui secondary menu">
{{if .PullRequestCtx.Allowed}} {{if .PullRequestCtx.Allowed}}
<div class="fitted item"> <div class="fitted item">

View File

@ -0,0 +1,14 @@
<div class="ui segment sub-menu">
<div class="ui two horizontal center link list">
{{if and (.Repository.UnitEnabled $.UnitTypeCode) (not .IsBareRepo)}}
<div class="item{{if .PageIsCommits}} active{{end}}">
<a href="{{.RepoLink}}/commits/{{EscapePound .BranchName}}"><i class="octicon octicon-history"></i> <b>{{.CommitsCount}}</b> {{.i18n.Tr "repo.commits"}}</a>
</div>
{{end}}
{{if and (.Repository.UnitEnabled $.UnitTypeCode) (not .IsBareRepo) }}
<div class="item{{if .PageIsBranches}} active{{end}}">
<a href="{{.RepoLink}}/branches/"><i class="octicon octicon-git-branch"></i> <b>{{.BrancheCount}}</b> {{.i18n.Tr "repo.branches"}}</a>
</div>
{{end}}
</div>
</div>