mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-01 05:36:19 +01:00
Add branch overiew page (#2108)
* Add branch overiew page * fix changed method name on sub menu * remove unused code
This commit is contained in:
parent
e86a0bf3fe
commit
3ab580c8d6
21 changed files with 701 additions and 52 deletions
79
integrations/branches_test.go
Normal file
79
integrations/branches_test.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
89
models/branches_test.go
Normal file
89
models/branches_test.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
|
|
29
models/migrations/v47.go
Normal file
29
models/migrations/v47.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -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)
|
||||
return
|
||||
} else if len(brs) == 0 {
|
||||
ctx.Handle(404, "repo.Branches(GetBranches)", nil)
|
||||
log.Error(4, "DeleteBranch: %v", err)
|
||||
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", branchName))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Branches"] = brs
|
||||
ctx.HTML(200, tplBranch)
|
||||
if isProtected {
|
||||
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
|
||||
|
|
|
@ -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)
|
||||
|
|
81
templates/repo/branch/list.tmpl
Normal file
81
templates/repo/branch/list.tmpl
Normal 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" .}}
|
|
@ -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">
|
||||
|
|
14
templates/repo/sub_menu.tmpl
Normal file
14
templates/repo/sub_menu.tmpl
Normal 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>
|
Loading…
Reference in a new issue