* Add branch overiew page * fix changed method name on sub menu * remove unused codetags/v1.3.0-rc1
@@ -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 | |||
} |
@@ -11,6 +11,7 @@ import ( | |||
"code.gitea.io/gitea/modules/base" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/modules/util" | |||
"github.com/Unknwon/com" | |||
@@ -193,3 +194,109 @@ func (repo *Repository) DeleteProtectedBranch(id int64) (err error) { | |||
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) | |||
} | |||
} |
@@ -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 | |||
} |
@@ -142,6 +142,8 @@ var migrations = []Migration{ | |||
NewMigration("remove index column from repo_unit table", removeIndexColumnFromRepoUnitTable), | |||
// v46 -> v47 | |||
NewMigration("remove organization watch repositories", removeOrganizationWatchRepo), | |||
// v47 -> v48 | |||
NewMigration("add deleted branches", addDeletedBranch), | |||
} | |||
// Migrate database to current version |
@@ -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 | |||
} |
@@ -114,6 +114,7 @@ func init() { | |||
new(CommitStatus), | |||
new(Stopwatch), | |||
new(TrackedTime), | |||
new(DeletedBranch), | |||
) | |||
gonicNames := []string{"SSL", "UID"} |
@@ -77,6 +77,17 @@ func NewContext() { | |||
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() | |||
} | |||
@@ -365,6 +365,12 @@ var ( | |||
Schedule string | |||
UpdateExisting bool | |||
} `ini:"cron.sync_external_users"` | |||
DeletedBranchesCleanup struct { | |||
Enabled bool | |||
RunAtStart bool | |||
Schedule string | |||
OlderThan time.Duration | |||
} `ini:"cron.deleted_branches_cleanup"` | |||
}{ | |||
UpdateMirror: struct { | |||
Enabled bool | |||
@@ -419,6 +425,17 @@ var ( | |||
Schedule: "@every 24h", | |||
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 |
@@ -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.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_html = Delete Branch | |||
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_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_failed = Failed to delete branch %s. | |||
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_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.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_name_holder = Organization Name |
@@ -1423,29 +1423,18 @@ $(document).ready(function () { | |||
}); | |||
// Helpers. | |||
$('.delete-button').click(function () { | |||
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; | |||
} | |||
$('.delete-button').click(showDeletePopup); | |||
$.post($this.data('url'), { | |||
"_csrf": csrf, | |||
"id": $this.data("id") | |||
}).done(function (data) { | |||
window.location.href = data.redirect; | |||
}); | |||
} | |||
}).modal('show'); | |||
return false; | |||
$('.delete-branch-button').click(showDeletePopup); | |||
$('.undo-button').click(function() { | |||
var $this = $(this); | |||
$.post($this.data('url'), { | |||
"_csrf": csrf, | |||
"id": $this.data("id") | |||
}).done(function(data) { | |||
window.location.href = data.redirect; | |||
}); | |||
}); | |||
$('.show-panel.button').click(function () { | |||
$($(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(){ | |||
var vueDelimeters = ['${', '}']; | |||
@@ -9,7 +9,7 @@ | |||
margin-bottom: 15px !important; | |||
background-color: #FAFAFA !important; | |||
border-width: 1px !important; | |||
.octicon { | |||
width: 16px; | |||
text-align: center; | |||
@@ -33,7 +33,7 @@ | |||
.name { | |||
word-break: break-all; | |||
} | |||
.metas { | |||
color: #888; | |||
font-size: 14px; | |||
@@ -50,6 +50,13 @@ | |||
} | |||
} | |||
.ui.repository.branches { | |||
.time{ | |||
font-size: 12px; | |||
color: #808080; | |||
} | |||
} | |||
.ui.user.list { | |||
.item { | |||
padding-bottom: 25px; |
@@ -1313,6 +1313,27 @@ | |||
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 | |||
@@ -5,32 +5,192 @@ | |||
package repo | |||
import ( | |||
"strings" | |||
"code.gitea.io/git" | |||
"code.gitea.io/gitea/models" | |||
"code.gitea.io/gitea/modules/auth" | |||
"code.gitea.io/gitea/modules/base" | |||
"code.gitea.io/gitea/modules/context" | |||
"code.gitea.io/gitea/modules/log" | |||
) | |||
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 | |||
func Branches(ctx *context.Context) { | |||
ctx.Data["Title"] = "Branches" | |||
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 { | |||
ctx.Handle(500, "repo.Branches(GetBranches)", err) | |||
log.Error(4, "DeleteBranch: %v", err) | |||
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", branchName)) | |||
return | |||
} else if len(brs) == 0 { | |||
ctx.Handle(404, "repo.Branches(GetBranches)", nil) | |||
} | |||
if isProtected { | |||
ctx.Flash.Error(ctx.Tr("repo.branch.protected_deletion_failed", branchName)) | |||
return | |||
} | |||
ctx.Data["Branches"] = brs | |||
ctx.HTML(200, tplBranch) | |||
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 |
@@ -53,6 +53,7 @@ func Commits(ctx *context.Context) { | |||
ctx.Handle(404, "Commit not found", nil) | |||
return | |||
} | |||
ctx.Data["PageIsViewCode"] = true | |||
commitsCount, err := ctx.Repo.Commit.CommitsCount() | |||
if err != nil { | |||
@@ -88,6 +89,7 @@ func Commits(ctx *context.Context) { | |||
// Graph render commit graph - show commits from all branches. | |||
func Graph(ctx *context.Context) { | |||
ctx.Data["PageIsCommits"] = true | |||
ctx.Data["PageIsViewCode"] = true | |||
commitsCount, err := ctx.Repo.Commit.CommitsCount() | |||
if err != nil { | |||
@@ -114,6 +116,7 @@ func Graph(ctx *context.Context) { | |||
// SearchCommits render commits filtered by keyword | |||
func SearchCommits(ctx *context.Context) { | |||
ctx.Data["PageIsCommits"] = true | |||
ctx.Data["PageIsViewCode"] = true | |||
keyword := strings.Trim(ctx.Query("q"), " ") | |||
if len(keyword) == 0 { |
@@ -550,7 +550,10 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
m.Group("/branches", func() { | |||
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()) | |||
// Releases | |||
@@ -615,6 +618,10 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
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.Get("/commits", context.RepoRef(), repo.ViewPullCommits) | |||
m.Get("/files", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ViewPullFiles) |
@@ -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" .}} |
@@ -2,18 +2,19 @@ | |||
<div class="repository commits"> | |||
{{template "repo/header" .}} | |||
<div class="ui container"> | |||
<div class="ui secondary menu"> | |||
{{template "repo/branch_dropdown" .}} | |||
<div class="fitted item"> | |||
<a href="{{.RepoLink}}/graph" class="ui basic small button"> | |||
<span class="text"> | |||
<i class="octicon octicon-git-branch"></i> | |||
</span> | |||
{{.i18n.Tr "repo.commit_graph"}} | |||
</a> | |||
</div> | |||
</div> | |||
{{template "repo/commits_table" .}} | |||
{{template "repo/sub_menu" .}} | |||
<div class="ui secondary menu"> | |||
{{template "repo/branch_dropdown" .}} | |||
<div class="fitted item"> | |||
<a href="{{.RepoLink}}/graph" class="ui basic small button"> | |||
<span class="text"> | |||
<i class="octicon octicon-git-branch"></i> | |||
</span> | |||
{{.i18n.Tr "repo.commit_graph"}} | |||
</a> | |||
</div> | |||
</div> | |||
{{template "repo/commits_table" .}} | |||
</div> | |||
</div> | |||
{{template "base/footer" .}} |
@@ -73,12 +73,6 @@ | |||
</a> | |||
{{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) }} | |||
<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> |
@@ -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}} | |||
<a class="link" href="{{.Repository.Website}}">{{.Repository.Website}}</a> | |||
</p> | |||
{{template "repo/sub_menu" .}} | |||
<div class="ui secondary menu"> | |||
{{if .PullRequestCtx.Allowed}} | |||
<div class="fitted item"> |
@@ -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> |