* Add branch overiew page * fix changed method name on sub menu * remove unused codetags/v1.21.12.1
| @@ -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> | |||