Add "Update Branch" button to Pull Requests (#9784)
* add Divergence * add Update Button * first working version * re-use code * split raw merge commands and db-change functions (notify, cache, ...) * use rawMerge (remove redundant code) * own function to get Diverging of PRs * use FlashError * correct Error Msg * hook is triggerd ... so remove comment * add "branch2" to "user2/repo1" because it unit-test "TestPullView_ReviewerMissed" use it but dont exist jet :/ * move GetPerm to IsUserAllowedToUpdate * add Flash Success MSG * imprufe code - remove useless js chage * fix-lint * TEST: add PullRequest ID:5 Repo: user2/repo1 Base: branch1 Head: pr-to-update * correct comments * make PR5 outdated * fix Tests * WIP: add pull update test * update revs * update locales * working TEST * update UI * misspell * change style * add 1s delay so rev exist * move row up (before merge row) * fix lint nit * UI remove divider * Update style * nits * do it right * introduce IsSameRepo * remove useless check Co-authored-by: Lauris BH <lauris@nix.lv>release/v1.15
parent
9f40bb020e
commit
36943e56d6
|
@ -134,7 +134,7 @@ func TestAPISearchIssue(t *testing.T) {
|
||||||
var apiIssues []*api.Issue
|
var apiIssues []*api.Issue
|
||||||
DecodeJSON(t, resp, &apiIssues)
|
DecodeJSON(t, resp, &apiIssues)
|
||||||
|
|
||||||
assert.Len(t, apiIssues, 8)
|
assert.Len(t, apiIssues, 9)
|
||||||
|
|
||||||
query := url.Values{}
|
query := url.Values{}
|
||||||
query.Add("token", token)
|
query.Add("token", token)
|
||||||
|
@ -142,7 +142,7 @@ func TestAPISearchIssue(t *testing.T) {
|
||||||
req = NewRequest(t, "GET", link.String())
|
req = NewRequest(t, "GET", link.String())
|
||||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
DecodeJSON(t, resp, &apiIssues)
|
DecodeJSON(t, resp, &apiIssues)
|
||||||
assert.Len(t, apiIssues, 8)
|
assert.Len(t, apiIssues, 9)
|
||||||
|
|
||||||
query.Add("state", "closed")
|
query.Add("state", "closed")
|
||||||
link.RawQuery = query.Encode()
|
link.RawQuery = query.Encode()
|
||||||
|
@ -163,5 +163,5 @@ func TestAPISearchIssue(t *testing.T) {
|
||||||
req = NewRequest(t, "GET", link.String())
|
req = NewRequest(t, "GET", link.String())
|
||||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
DecodeJSON(t, resp, &apiIssues)
|
DecodeJSON(t, resp, &apiIssues)
|
||||||
assert.Len(t, apiIssues, 0)
|
assert.Len(t, apiIssues, 1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
65f1bf27bc3bf70f64657658635e66094edbcb4d refs/heads/master
|
65f1bf27bc3bf70f64657658635e66094edbcb4d refs/heads/master
|
||||||
|
985f0301dba5e7b34be866819cd15ad3d8f508ee refs/heads/branch2
|
||||||
|
62fb502a7172d4453f0322a2cc85bddffa57f07a refs/heads/pr-to-update
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
||||||
|
985f0301dba5e7b34be866819cd15ad3d8f508ee
|
|
@ -0,0 +1 @@
|
||||||
|
62fb502a7172d4453f0322a2cc85bddffa57f07a
|
|
@ -0,0 +1 @@
|
||||||
|
4a357436d925b5c974181ff12a994538ddc5a269
|
|
@ -0,0 +1 @@
|
||||||
|
62fb502a7172d4453f0322a2cc85bddffa57f07a
|
|
@ -0,0 +1,136 @@
|
||||||
|
// Copyright 2020 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 (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models"
|
||||||
|
"code.gitea.io/gitea/modules/repofiles"
|
||||||
|
repo_module "code.gitea.io/gitea/modules/repository"
|
||||||
|
pull_service "code.gitea.io/gitea/services/pull"
|
||||||
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPullUpdate(t *testing.T) {
|
||||||
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||||
|
//Create PR to test
|
||||||
|
user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
|
||||||
|
org26 := models.AssertExistsAndLoadBean(t, &models.User{ID: 26}).(*models.User)
|
||||||
|
pr := createOutdatedPR(t, user, org26)
|
||||||
|
|
||||||
|
//Test GetDiverging
|
||||||
|
diffCount, err := pull_service.GetDiverging(pr)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, 1, diffCount.Behind)
|
||||||
|
assert.EqualValues(t, 1, diffCount.Ahead)
|
||||||
|
|
||||||
|
message := fmt.Sprintf("Merge branch '%s' into %s", pr.BaseBranch, pr.HeadBranch)
|
||||||
|
err = pull_service.Update(pr, user, message)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
//Test GetDiverging after update
|
||||||
|
diffCount, err = pull_service.GetDiverging(pr)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, 0, diffCount.Behind)
|
||||||
|
assert.EqualValues(t, 2, diffCount.Ahead)
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func createOutdatedPR(t *testing.T, actor, forkOrg *models.User) *models.PullRequest {
|
||||||
|
baseRepo, err := repo_service.CreateRepository(actor, actor, models.CreateRepoOptions{
|
||||||
|
Name: "repo-pr-update",
|
||||||
|
Description: "repo-tmp-pr-update description",
|
||||||
|
AutoInit: true,
|
||||||
|
Gitignores: "C,C++",
|
||||||
|
License: "MIT",
|
||||||
|
Readme: "Default",
|
||||||
|
IsPrivate: false,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, baseRepo)
|
||||||
|
|
||||||
|
headRepo, err := repo_module.ForkRepository(actor, forkOrg, baseRepo, "repo-pr-update", "desc")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, headRepo)
|
||||||
|
|
||||||
|
//create a commit on base Repo
|
||||||
|
_, err = repofiles.CreateOrUpdateRepoFile(baseRepo, actor, &repofiles.UpdateRepoFileOptions{
|
||||||
|
TreePath: "File_A",
|
||||||
|
Message: "Add File A",
|
||||||
|
Content: "File A",
|
||||||
|
IsNewFile: true,
|
||||||
|
OldBranch: "master",
|
||||||
|
NewBranch: "master",
|
||||||
|
Author: &repofiles.IdentityOptions{
|
||||||
|
Name: actor.Name,
|
||||||
|
Email: actor.Email,
|
||||||
|
},
|
||||||
|
Committer: &repofiles.IdentityOptions{
|
||||||
|
Name: actor.Name,
|
||||||
|
Email: actor.Email,
|
||||||
|
},
|
||||||
|
Dates: &repofiles.CommitDateOptions{
|
||||||
|
Author: time.Now(),
|
||||||
|
Committer: time.Now(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
//create a commit on head Repo
|
||||||
|
_, err = repofiles.CreateOrUpdateRepoFile(headRepo, actor, &repofiles.UpdateRepoFileOptions{
|
||||||
|
TreePath: "File_B",
|
||||||
|
Message: "Add File on PR branch",
|
||||||
|
Content: "File B",
|
||||||
|
IsNewFile: true,
|
||||||
|
OldBranch: "master",
|
||||||
|
NewBranch: "newBranch",
|
||||||
|
Author: &repofiles.IdentityOptions{
|
||||||
|
Name: actor.Name,
|
||||||
|
Email: actor.Email,
|
||||||
|
},
|
||||||
|
Committer: &repofiles.IdentityOptions{
|
||||||
|
Name: actor.Name,
|
||||||
|
Email: actor.Email,
|
||||||
|
},
|
||||||
|
Dates: &repofiles.CommitDateOptions{
|
||||||
|
Author: time.Now(),
|
||||||
|
Committer: time.Now(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
//create Pull
|
||||||
|
pullIssue := &models.Issue{
|
||||||
|
RepoID: baseRepo.ID,
|
||||||
|
Title: "Test Pull -to-update-",
|
||||||
|
PosterID: actor.ID,
|
||||||
|
Poster: actor,
|
||||||
|
IsPull: true,
|
||||||
|
}
|
||||||
|
pullRequest := &models.PullRequest{
|
||||||
|
HeadRepoID: headRepo.ID,
|
||||||
|
BaseRepoID: baseRepo.ID,
|
||||||
|
HeadBranch: "newBranch",
|
||||||
|
BaseBranch: "master",
|
||||||
|
HeadRepo: headRepo,
|
||||||
|
BaseRepo: baseRepo,
|
||||||
|
Type: models.PullRequestGitea,
|
||||||
|
}
|
||||||
|
err = pull_service.NewPullRequest(baseRepo, pullIssue, nil, nil, pullRequest, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
issue := models.AssertExistsAndLoadBean(t, &models.Issue{Title: "Test Pull -to-update-"}).(*models.Issue)
|
||||||
|
pr, err := models.GetPullRequestByIssueID(issue.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
return pr
|
||||||
|
}
|
|
@ -56,9 +56,9 @@ func TestRepoActivity(t *testing.T) {
|
||||||
list = htmlDoc.doc.Find("#merged-pull-requests").Next().Find("p.desc")
|
list = htmlDoc.doc.Find("#merged-pull-requests").Next().Find("p.desc")
|
||||||
assert.Len(t, list.Nodes, 1)
|
assert.Len(t, list.Nodes, 1)
|
||||||
|
|
||||||
// Should be 2 merged proposed pull requests
|
// Should be 3 merged proposed pull requests
|
||||||
list = htmlDoc.doc.Find("#proposed-pull-requests").Next().Find("p.desc")
|
list = htmlDoc.doc.Find("#proposed-pull-requests").Next().Find("p.desc")
|
||||||
assert.Len(t, list.Nodes, 2)
|
assert.Len(t, list.Nodes, 3)
|
||||||
|
|
||||||
// Should be 3 new issues
|
// Should be 3 new issues
|
||||||
list = htmlDoc.doc.Find("#new-issues").Next().Find("p.desc")
|
list = htmlDoc.doc.Find("#new-issues").Next().Find("p.desc")
|
||||||
|
|
|
@ -122,3 +122,15 @@
|
||||||
created_unix: 946684830
|
created_unix: 946684830
|
||||||
updated_unix: 999307200
|
updated_unix: 999307200
|
||||||
deadline_unix: 1019307200
|
deadline_unix: 1019307200
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 11
|
||||||
|
repo_id: 1
|
||||||
|
index: 5
|
||||||
|
poster_id: 1
|
||||||
|
name: pull5
|
||||||
|
content: content for the a pull request
|
||||||
|
is_closed: false
|
||||||
|
is_pull: true
|
||||||
|
created_unix: 1579194806
|
||||||
|
updated_unix: 1579194806
|
||||||
|
|
|
@ -50,3 +50,16 @@
|
||||||
base_branch: master
|
base_branch: master
|
||||||
merge_base: abcdef1234567890
|
merge_base: abcdef1234567890
|
||||||
has_merged: false
|
has_merged: false
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 5 # this PR is outdated (one commit behind branch1 )
|
||||||
|
type: 0 # gitea pull request
|
||||||
|
status: 2 # mergable
|
||||||
|
issue_id: 11
|
||||||
|
index: 5
|
||||||
|
head_repo_id: 1
|
||||||
|
base_repo_id: 1
|
||||||
|
head_branch: pr-to-update
|
||||||
|
base_branch: branch1
|
||||||
|
merge_base: 1234567890abcdef
|
||||||
|
has_merged: false
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
is_private: false
|
is_private: false
|
||||||
num_issues: 2
|
num_issues: 2
|
||||||
num_closed_issues: 1
|
num_closed_issues: 1
|
||||||
num_pulls: 2
|
num_pulls: 3
|
||||||
num_closed_pulls: 0
|
num_closed_pulls: 0
|
||||||
num_milestones: 3
|
num_milestones: 3
|
||||||
num_closed_milestones: 1
|
num_closed_milestones: 1
|
||||||
|
|
|
@ -276,8 +276,8 @@ func TestIssue_SearchIssueIDsByKeyword(t *testing.T) {
|
||||||
|
|
||||||
total, ids, err = SearchIssueIDsByKeyword("for", []int64{1}, 10, 0)
|
total, ids, err = SearchIssueIDsByKeyword("for", []int64{1}, 10, 0)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.EqualValues(t, 4, total)
|
assert.EqualValues(t, 5, total)
|
||||||
assert.EqualValues(t, []int64{1, 2, 3, 5}, ids)
|
assert.EqualValues(t, []int64{1, 2, 3, 5, 11}, ids)
|
||||||
|
|
||||||
// issue1's comment id 2
|
// issue1's comment id 2
|
||||||
total, ids, err = SearchIssueIDsByKeyword("good", []int64{1}, 10, 0)
|
total, ids, err = SearchIssueIDsByKeyword("good", []int64{1}, 10, 0)
|
||||||
|
@ -305,8 +305,8 @@ func testInsertIssue(t *testing.T, title, content string) {
|
||||||
assert.True(t, has)
|
assert.True(t, has)
|
||||||
assert.EqualValues(t, issue.Title, newIssue.Title)
|
assert.EqualValues(t, issue.Title, newIssue.Title)
|
||||||
assert.EqualValues(t, issue.Content, newIssue.Content)
|
assert.EqualValues(t, issue.Content, newIssue.Content)
|
||||||
// there are 4 issues and max index is 4 on repository 1, so this one should 5
|
// there are 5 issues and max index is 5 on repository 1, so this one should 6
|
||||||
assert.EqualValues(t, 5, newIssue.Index)
|
assert.EqualValues(t, 6, newIssue.Index)
|
||||||
|
|
||||||
_, err = x.ID(issue.ID).Delete(new(Issue))
|
_, err = x.ID(issue.ID).Delete(new(Issue))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
@ -17,7 +17,7 @@ func Test_newIssueUsers(t *testing.T) {
|
||||||
newIssue := &Issue{
|
newIssue := &Issue{
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
PosterID: 4,
|
PosterID: 4,
|
||||||
Index: 5,
|
Index: 6,
|
||||||
Title: "newTestIssueTitle",
|
Title: "newTestIssueTitle",
|
||||||
Content: "newTestIssueContent",
|
Content: "newTestIssueContent",
|
||||||
}
|
}
|
||||||
|
|
|
@ -742,3 +742,8 @@ func (pr *PullRequest) IsHeadEqualWithBranch(branchName string) (bool, error) {
|
||||||
}
|
}
|
||||||
return baseCommit.HasPreviousCommit(headCommit.ID)
|
return baseCommit.HasPreviousCommit(headCommit.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsSameRepo returns true if base repo and head repo is the same
|
||||||
|
func (pr *PullRequest) IsSameRepo() bool {
|
||||||
|
return pr.BaseRepoID == pr.HeadRepoID
|
||||||
|
}
|
||||||
|
|
|
@ -61,10 +61,11 @@ func TestPullRequestsNewest(t *testing.T) {
|
||||||
Labels: []string{},
|
Labels: []string{},
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(2), count)
|
assert.EqualValues(t, 3, count)
|
||||||
if assert.Len(t, prs, 2) {
|
if assert.Len(t, prs, 3) {
|
||||||
assert.Equal(t, int64(2), prs[0].ID)
|
assert.EqualValues(t, 5, prs[0].ID)
|
||||||
assert.Equal(t, int64(1), prs[1].ID)
|
assert.EqualValues(t, 2, prs[1].ID)
|
||||||
|
assert.EqualValues(t, 1, prs[2].ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,10 +78,11 @@ func TestPullRequestsOldest(t *testing.T) {
|
||||||
Labels: []string{},
|
Labels: []string{},
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(2), count)
|
assert.EqualValues(t, 3, count)
|
||||||
if assert.Len(t, prs, 2) {
|
if assert.Len(t, prs, 3) {
|
||||||
assert.Equal(t, int64(1), prs[0].ID)
|
assert.EqualValues(t, 1, prs[0].ID)
|
||||||
assert.Equal(t, int64(2), prs[1].ID)
|
assert.EqualValues(t, 2, prs[1].ID)
|
||||||
|
assert.EqualValues(t, 5, prs[2].ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,7 @@ func TestBleveSearchIssues(t *testing.T) {
|
||||||
|
|
||||||
ids, err = SearchIssuesByKeyword([]int64{1}, "for")
|
ids, err = SearchIssuesByKeyword([]int64{1}, "for")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.EqualValues(t, []int64{1, 2, 3, 5}, ids)
|
assert.EqualValues(t, []int64{1, 2, 3, 5, 11}, ids)
|
||||||
|
|
||||||
ids, err = SearchIssuesByKeyword([]int64{1}, "good")
|
ids, err = SearchIssuesByKeyword([]int64{1}, "good")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -89,7 +89,7 @@ func TestDBSearchIssues(t *testing.T) {
|
||||||
|
|
||||||
ids, err = SearchIssuesByKeyword([]int64{1}, "for")
|
ids, err = SearchIssuesByKeyword([]int64{1}, "for")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.EqualValues(t, []int64{1, 2, 3, 5}, ids)
|
assert.EqualValues(t, []int64{1, 2, 3, 5, 11}, ids)
|
||||||
|
|
||||||
ids, err = SearchIssuesByKeyword([]int64{1}, "good")
|
ids, err = SearchIssuesByKeyword([]int64{1}, "good")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
@ -1082,6 +1082,10 @@ pulls.open_unmerged_pull_exists = `You cannot perform a reopen operation because
|
||||||
pulls.status_checking = Some checks are pending
|
pulls.status_checking = Some checks are pending
|
||||||
pulls.status_checks_success = All checks were successful
|
pulls.status_checks_success = All checks were successful
|
||||||
pulls.status_checks_error = Some checks failed
|
pulls.status_checks_error = Some checks failed
|
||||||
|
pulls.update_branch = Update branch
|
||||||
|
pulls.update_branch_success = Branch update was successful
|
||||||
|
pulls.update_not_allowed = You are not allowed to update branch
|
||||||
|
pulls.outdated_with_base_branch = This branch is out-of-date with the base branch
|
||||||
|
|
||||||
milestones.new = New Milestone
|
milestones.new = New Milestone
|
||||||
milestones.open_tab = %d Open
|
milestones.open_tab = %d Open
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/auth"
|
"code.gitea.io/gitea/modules/auth"
|
||||||
|
@ -342,8 +343,21 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare
|
||||||
|
|
||||||
setMergeTarget(ctx, pull)
|
setMergeTarget(ctx, pull)
|
||||||
|
|
||||||
|
divergence, err := pull_service.GetDiverging(pull)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetDiverging", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctx.Data["Divergence"] = divergence
|
||||||
|
allowUpdate, err := pull_service.IsUserAllowedToUpdate(pull, ctx.User)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetDiverging", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctx.Data["UpdateAllowed"] = allowUpdate
|
||||||
|
|
||||||
if err := pull.LoadProtectedBranch(); err != nil {
|
if err := pull.LoadProtectedBranch(); err != nil {
|
||||||
ctx.ServerError("GetLatestCommitStatus", err)
|
ctx.ServerError("LoadProtectedBranch", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ctx.Data["EnableStatusCheck"] = pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck
|
ctx.Data["EnableStatusCheck"] = pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck
|
||||||
|
@ -587,6 +601,72 @@ func ViewPullFiles(ctx *context.Context) {
|
||||||
ctx.HTML(200, tplPullFiles)
|
ctx.HTML(200, tplPullFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdatePullRequest merge master into PR
|
||||||
|
func UpdatePullRequest(ctx *context.Context) {
|
||||||
|
issue := checkPullInfo(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if issue.IsClosed {
|
||||||
|
ctx.NotFound("MergePullRequest", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if issue.PullRequest.HasMerged {
|
||||||
|
ctx.NotFound("MergePullRequest", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := issue.PullRequest.LoadBaseRepo(); err != nil {
|
||||||
|
ctx.InternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := issue.PullRequest.LoadHeadRepo(); err != nil {
|
||||||
|
ctx.InternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedUpdate, err := pull_service.IsUserAllowedToUpdate(issue.PullRequest, ctx.User)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("IsUserAllowedToMerge", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToDo: add check if maintainers are allowed to change branch ... (need migration & co)
|
||||||
|
if !allowedUpdate {
|
||||||
|
ctx.Flash.Error(ctx.Tr("repo.pulls.update_not_allowed"))
|
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// default merge commit message
|
||||||
|
message := fmt.Sprintf("Merge branch '%s' into %s", issue.PullRequest.BaseBranch, issue.PullRequest.HeadBranch)
|
||||||
|
|
||||||
|
if err = pull_service.Update(issue.PullRequest, ctx.User, message); err != nil {
|
||||||
|
sanitize := func(x string) string {
|
||||||
|
runes := []rune(x)
|
||||||
|
|
||||||
|
if len(runes) > 512 {
|
||||||
|
x = "..." + string(runes[len(runes)-512:])
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Replace(html.EscapeString(x), "\n", "<br>", -1)
|
||||||
|
}
|
||||||
|
if models.IsErrMergeConflicts(err) {
|
||||||
|
conflictError := err.(models.ErrMergeConflicts)
|
||||||
|
ctx.Flash.Error(ctx.Tr("repo.pulls.merge_conflict", sanitize(conflictError.StdErr), sanitize(conflictError.StdOut)))
|
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Flash.Error(err.Error())
|
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
ctx.Flash.Success(ctx.Tr("repo.pulls.update_branch_success"))
|
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
|
||||||
|
}
|
||||||
|
|
||||||
// MergePullRequest response for merging pull request
|
// MergePullRequest response for merging pull request
|
||||||
func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) {
|
func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) {
|
||||||
issue := checkPullInfo(ctx)
|
issue := checkPullInfo(ctx)
|
||||||
|
|
|
@ -855,6 +855,7 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||||
m.Get(".patch", repo.DownloadPullPatch)
|
m.Get(".patch", repo.DownloadPullPatch)
|
||||||
m.Get("/commits", context.RepoRef(), repo.ViewPullCommits)
|
m.Get("/commits", context.RepoRef(), repo.ViewPullCommits)
|
||||||
m.Post("/merge", context.RepoMustNotBeArchived(), reqRepoPullsWriter, bindIgnErr(auth.MergePullRequestForm{}), repo.MergePullRequest)
|
m.Post("/merge", context.RepoMustNotBeArchived(), reqRepoPullsWriter, bindIgnErr(auth.MergePullRequestForm{}), repo.MergePullRequest)
|
||||||
|
m.Post("/update", repo.UpdatePullRequest)
|
||||||
m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest)
|
m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest)
|
||||||
m.Group("/files", func() {
|
m.Group("/files", func() {
|
||||||
m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.ViewPullFiles)
|
m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.ViewPullFiles)
|
||||||
|
|
|
@ -33,11 +33,6 @@ import (
|
||||||
// Caller should check PR is ready to be merged (review and status checks)
|
// Caller should check PR is ready to be merged (review and status checks)
|
||||||
// FIXME: add repoWorkingPull make sure two merges does not happen at same time.
|
// FIXME: add repoWorkingPull make sure two merges does not happen at same time.
|
||||||
func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repository, mergeStyle models.MergeStyle, message string) (err error) {
|
func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repository, mergeStyle models.MergeStyle, message string) (err error) {
|
||||||
binVersion, err := git.BinVersion()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("git.BinVersion: %v", err)
|
|
||||||
return fmt.Errorf("Unable to get git version: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = pr.GetHeadRepo(); err != nil {
|
if err = pr.GetHeadRepo(); err != nil {
|
||||||
log.Error("GetHeadRepo: %v", err)
|
log.Error("GetHeadRepo: %v", err)
|
||||||
|
@ -63,6 +58,61 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
|
||||||
go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "")
|
go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
if err := rawMerge(pr, doer, mergeStyle, message); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pr.MergedCommitID, err = baseGitRepo.GetBranchCommitID(pr.BaseBranch)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("GetBranchCommit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pr.MergedUnix = timeutil.TimeStampNow()
|
||||||
|
pr.Merger = doer
|
||||||
|
pr.MergerID = doer.ID
|
||||||
|
|
||||||
|
if err = pr.SetMerged(); err != nil {
|
||||||
|
log.Error("setMerged [%d]: %v", pr.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.NotifyMergePullRequest(pr, doer)
|
||||||
|
|
||||||
|
// Reset cached commit count
|
||||||
|
cache.Remove(pr.Issue.Repo.GetCommitsCountCacheKey(pr.BaseBranch, true))
|
||||||
|
|
||||||
|
// Resolve cross references
|
||||||
|
refs, err := pr.ResolveCrossReferences()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("ResolveCrossReferences: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ref := range refs {
|
||||||
|
if err = ref.LoadIssue(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = ref.Issue.LoadRepo(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
close := (ref.RefAction == references.XRefActionCloses)
|
||||||
|
if close != ref.Issue.IsClosed {
|
||||||
|
if err = issue_service.ChangeStatus(ref.Issue, doer, close); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// rawMerge perform the merge operation without changing any pull information in database
|
||||||
|
func rawMerge(pr *models.PullRequest, doer *models.User, mergeStyle models.MergeStyle, message string) (err error) {
|
||||||
|
binVersion, err := git.BinVersion()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("git.BinVersion: %v", err)
|
||||||
|
return fmt.Errorf("Unable to get git version: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Clone base repo.
|
// Clone base repo.
|
||||||
tmpBasePath, err := createTemporaryRepo(pr)
|
tmpBasePath, err := createTemporaryRepo(pr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -337,46 +387,6 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
|
||||||
outbuf.Reset()
|
outbuf.Reset()
|
||||||
errbuf.Reset()
|
errbuf.Reset()
|
||||||
|
|
||||||
pr.MergedCommitID, err = baseGitRepo.GetBranchCommitID(pr.BaseBranch)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("GetBranchCommit: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pr.MergedUnix = timeutil.TimeStampNow()
|
|
||||||
pr.Merger = doer
|
|
||||||
pr.MergerID = doer.ID
|
|
||||||
|
|
||||||
if err = pr.SetMerged(); err != nil {
|
|
||||||
log.Error("setMerged [%d]: %v", pr.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
notification.NotifyMergePullRequest(pr, doer)
|
|
||||||
|
|
||||||
// Reset cached commit count
|
|
||||||
cache.Remove(pr.Issue.Repo.GetCommitsCountCacheKey(pr.BaseBranch, true))
|
|
||||||
|
|
||||||
// Resolve cross references
|
|
||||||
refs, err := pr.ResolveCrossReferences()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("ResolveCrossReferences: %v", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ref := range refs {
|
|
||||||
if err = ref.LoadIssue(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = ref.Issue.LoadRepo(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
close := (ref.RefAction == references.XRefActionCloses)
|
|
||||||
if close != ref.Issue.IsClosed {
|
|
||||||
if err = issue_service.ChangeStatus(ref.Issue, doer, close); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
// Copyright 2020 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 pull
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update updates pull request with base branch.
|
||||||
|
func Update(pull *models.PullRequest, doer *models.User, message string) error {
|
||||||
|
//use merge functions but switch repo's and branch's
|
||||||
|
pr := &models.PullRequest{
|
||||||
|
HeadRepoID: pull.BaseRepoID,
|
||||||
|
BaseRepoID: pull.HeadRepoID,
|
||||||
|
HeadBranch: pull.BaseBranch,
|
||||||
|
BaseBranch: pull.HeadBranch,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pr.LoadHeadRepo(); err != nil {
|
||||||
|
log.Error("LoadHeadRepo: %v", err)
|
||||||
|
return fmt.Errorf("LoadHeadRepo: %v", err)
|
||||||
|
} else if err = pr.LoadBaseRepo(); err != nil {
|
||||||
|
log.Error("LoadBaseRepo: %v", err)
|
||||||
|
return fmt.Errorf("LoadBaseRepo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
diffCount, err := GetDiverging(pull)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if diffCount.Behind == 0 {
|
||||||
|
return fmt.Errorf("HeadBranch of PR %d is up to date", pull.Index)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
go AddTestPullRequestTask(doer, pr.HeadRepo.ID, pr.HeadBranch, false, "", "")
|
||||||
|
}()
|
||||||
|
|
||||||
|
return rawMerge(pr, doer, models.MergeStyleMerge, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUserAllowedToUpdate check if user is allowed to update PR with given permissions and branch protections
|
||||||
|
func IsUserAllowedToUpdate(pull *models.PullRequest, user *models.User) (bool, error) {
|
||||||
|
headRepoPerm, err := models.GetUserRepoPermission(pull.HeadRepo, user)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pr := &models.PullRequest{
|
||||||
|
HeadRepoID: pull.BaseRepoID,
|
||||||
|
BaseRepoID: pull.HeadRepoID,
|
||||||
|
HeadBranch: pull.BaseBranch,
|
||||||
|
BaseBranch: pull.HeadBranch,
|
||||||
|
}
|
||||||
|
return IsUserAllowedToMerge(pr, headRepoPerm, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDiverging determines how many commits a PR is ahead or behind the PR base branch
|
||||||
|
func GetDiverging(pr *models.PullRequest) (*git.DivergeObject, error) {
|
||||||
|
log.Trace("PushToBaseRepo[%d]: pushing commits to base repo '%s'", pr.BaseRepoID, pr.GetGitRefName())
|
||||||
|
if err := pr.LoadBaseRepo(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := pr.LoadHeadRepo(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
headRepoPath := pr.HeadRepo.RepoPath()
|
||||||
|
headGitRepo, err := git.OpenRepository(headRepoPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("OpenRepository: %v", err)
|
||||||
|
}
|
||||||
|
defer headGitRepo.Close()
|
||||||
|
|
||||||
|
if pr.IsSameRepo() {
|
||||||
|
diff, err := git.GetDivergingCommits(pr.HeadRepo.RepoPath(), pr.BaseBranch, pr.HeadBranch)
|
||||||
|
return &diff, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpRemoteName := fmt.Sprintf("tmp-pull-%d-base", pr.ID)
|
||||||
|
if err = headGitRepo.AddRemote(tmpRemoteName, pr.BaseRepo.RepoPath(), true); err != nil {
|
||||||
|
return nil, fmt.Errorf("headGitRepo.AddRemote: %v", err)
|
||||||
|
}
|
||||||
|
// Make sure to remove the remote even if the push fails
|
||||||
|
defer func() {
|
||||||
|
if err := headGitRepo.RemoveRemote(tmpRemoteName); err != nil {
|
||||||
|
log.Error("CountDiverging: RemoveRemote: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// $(git rev-list --count tmp-pull-1-base/master..feature) commits ahead of master
|
||||||
|
ahead, errorAhead := checkDivergence(headRepoPath, fmt.Sprintf("%s/%s", tmpRemoteName, pr.BaseBranch), pr.HeadBranch)
|
||||||
|
if errorAhead != nil {
|
||||||
|
return &git.DivergeObject{}, errorAhead
|
||||||
|
}
|
||||||
|
|
||||||
|
// $(git rev-list --count feature..tmp-pull-1-base/master) commits behind master
|
||||||
|
behind, errorBehind := checkDivergence(headRepoPath, pr.HeadBranch, fmt.Sprintf("%s/%s", tmpRemoteName, pr.BaseBranch))
|
||||||
|
if errorBehind != nil {
|
||||||
|
return &git.DivergeObject{}, errorBehind
|
||||||
|
}
|
||||||
|
|
||||||
|
return &git.DivergeObject{Ahead: ahead, Behind: behind}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkDivergence(repoPath string, baseBranch string, targetBranch string) (int, error) {
|
||||||
|
branches := fmt.Sprintf("%s..%s", baseBranch, targetBranch)
|
||||||
|
cmd := git.NewCommand("rev-list", "--count", branches)
|
||||||
|
stdout, err := cmd.RunInDir(repoPath)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
outInteger, errInteger := strconv.Atoi(strings.Trim(stdout, "\n"))
|
||||||
|
if errInteger != nil {
|
||||||
|
return -1, errInteger
|
||||||
|
}
|
||||||
|
return outInteger, nil
|
||||||
|
}
|
|
@ -157,6 +157,26 @@
|
||||||
{{$.i18n.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason) }}
|
{{$.i18n.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason) }}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if and .Divergence (gt .Divergence.Behind 0)}}
|
||||||
|
<div class="ui very compact branch-update grid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="item text gray eleven wide left floated column">
|
||||||
|
<i class="icon icon-octicon"><span class="octicon octicon-alert"></span></i>
|
||||||
|
{{$.i18n.Tr "repo.pulls.outdated_with_base_branch"}}
|
||||||
|
</div>
|
||||||
|
{{if .UpdateAllowed}}
|
||||||
|
<div class="item text five wide right floated column">
|
||||||
|
<form action="{{.Link}}/update" method="post">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<button class="ui button" data-do="update">
|
||||||
|
<span class="item text">{{$.i18n.Tr "repo.pulls.update_branch"}}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
{{if .AllowMerge}}
|
{{if .AllowMerge}}
|
||||||
{{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}}
|
{{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}}
|
||||||
{{$approvers := .Issue.PullRequest.GetApprovers}}
|
{{$approvers := .Issue.PullRequest.GetApprovers}}
|
||||||
|
|
|
@ -655,6 +655,13 @@
|
||||||
.icon-octicon {
|
.icon-octicon {
|
||||||
padding-left: 2px;
|
padding-left: 2px;
|
||||||
}
|
}
|
||||||
|
.branch-update.grid {
|
||||||
|
margin-bottom: -1.5rem;
|
||||||
|
margin-top: -0.5rem;
|
||||||
|
.row {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-item {
|
.review-item {
|
||||||
|
|
Loading…
Reference in New Issue