diff --git a/integrations/api_issue_test.go b/integrations/api_issue_test.go
index ce1c4b7d33f..906dbb2dc7e 100644
--- a/integrations/api_issue_test.go
+++ b/integrations/api_issue_test.go
@@ -134,7 +134,7 @@ func TestAPISearchIssue(t *testing.T) {
var apiIssues []*api.Issue
DecodeJSON(t, resp, &apiIssues)
- assert.Len(t, apiIssues, 8)
+ assert.Len(t, apiIssues, 9)
query := url.Values{}
query.Add("token", token)
@@ -142,7 +142,7 @@ func TestAPISearchIssue(t *testing.T) {
req = NewRequest(t, "GET", link.String())
resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiIssues)
- assert.Len(t, apiIssues, 8)
+ assert.Len(t, apiIssues, 9)
query.Add("state", "closed")
link.RawQuery = query.Encode()
@@ -163,5 +163,5 @@ func TestAPISearchIssue(t *testing.T) {
req = NewRequest(t, "GET", link.String())
resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiIssues)
- assert.Len(t, apiIssues, 0)
+ assert.Len(t, apiIssues, 1)
}
diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/info/refs b/integrations/gitea-repositories-meta/user2/repo1.git/info/refs
index ca1df85e2eb..fa3009793de 100644
--- a/integrations/gitea-repositories-meta/user2/repo1.git/info/refs
+++ b/integrations/gitea-repositories-meta/user2/repo1.git/info/refs
@@ -1 +1,3 @@
65f1bf27bc3bf70f64657658635e66094edbcb4d refs/heads/master
+985f0301dba5e7b34be866819cd15ad3d8f508ee refs/heads/branch2
+62fb502a7172d4453f0322a2cc85bddffa57f07a refs/heads/pr-to-update
diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/5c/050d3b6d2db231ab1f64e324f1b6b9a0b181c2 b/integrations/gitea-repositories-meta/user2/repo1.git/objects/5c/050d3b6d2db231ab1f64e324f1b6b9a0b181c2
new file mode 100644
index 00000000000..c0cb626359e
Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/5c/050d3b6d2db231ab1f64e324f1b6b9a0b181c2 differ
diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/62/fb502a7172d4453f0322a2cc85bddffa57f07a b/integrations/gitea-repositories-meta/user2/repo1.git/objects/62/fb502a7172d4453f0322a2cc85bddffa57f07a
new file mode 100644
index 00000000000..ee494a8ca8c
Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/62/fb502a7172d4453f0322a2cc85bddffa57f07a differ
diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/6a/a3a5385611c5eb8986c9961a9c34a93cbaadfb b/integrations/gitea-repositories-meta/user2/repo1.git/objects/6a/a3a5385611c5eb8986c9961a9c34a93cbaadfb
new file mode 100644
index 00000000000..09aed946f2f
Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/6a/a3a5385611c5eb8986c9961a9c34a93cbaadfb differ
diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/7c/4df115542e05c700f297519e906fd63c9c9804 b/integrations/gitea-repositories-meta/user2/repo1.git/objects/7c/4df115542e05c700f297519e906fd63c9c9804
new file mode 100644
index 00000000000..3bf67a206ab
Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/7c/4df115542e05c700f297519e906fd63c9c9804 differ
diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/94/922e1295c678267de1193b7b84ad8a086c27f9 b/integrations/gitea-repositories-meta/user2/repo1.git/objects/94/922e1295c678267de1193b7b84ad8a086c27f9
new file mode 100644
index 00000000000..60692df6ec4
Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/94/922e1295c678267de1193b7b84ad8a086c27f9 differ
diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/98/5f0301dba5e7b34be866819cd15ad3d8f508ee b/integrations/gitea-repositories-meta/user2/repo1.git/objects/98/5f0301dba5e7b34be866819cd15ad3d8f508ee
new file mode 100644
index 00000000000..81fd6a50fda
Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/98/5f0301dba5e7b34be866819cd15ad3d8f508ee differ
diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/a6/9277c81e90b98a7c0ab25b042a6e296da8eb9a b/integrations/gitea-repositories-meta/user2/repo1.git/objects/a6/9277c81e90b98a7c0ab25b042a6e296da8eb9a
new file mode 100644
index 00000000000..887669883b7
Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/a6/9277c81e90b98a7c0ab25b042a6e296da8eb9a differ
diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/a7/57c0ea621e63d0fd6fc353a175fdc7199e5d1d b/integrations/gitea-repositories-meta/user2/repo1.git/objects/a7/57c0ea621e63d0fd6fc353a175fdc7199e5d1d
new file mode 100644
index 00000000000..c3111a08b84
Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/a7/57c0ea621e63d0fd6fc353a175fdc7199e5d1d differ
diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/b2/60587271671842af0b036e4fe643c9d45b7ddd b/integrations/gitea-repositories-meta/user2/repo1.git/objects/b2/60587271671842af0b036e4fe643c9d45b7ddd
new file mode 100644
index 00000000000..9182ac03816
Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/b2/60587271671842af0b036e4fe643c9d45b7ddd differ
diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/refs/heads/branch2 b/integrations/gitea-repositories-meta/user2/repo1.git/refs/heads/branch2
new file mode 100644
index 00000000000..5add7256cd9
--- /dev/null
+++ b/integrations/gitea-repositories-meta/user2/repo1.git/refs/heads/branch2
@@ -0,0 +1 @@
+985f0301dba5e7b34be866819cd15ad3d8f508ee
diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/refs/heads/pr-to-update b/integrations/gitea-repositories-meta/user2/repo1.git/refs/heads/pr-to-update
new file mode 100644
index 00000000000..e0ee44dd145
--- /dev/null
+++ b/integrations/gitea-repositories-meta/user2/repo1.git/refs/heads/pr-to-update
@@ -0,0 +1 @@
+62fb502a7172d4453f0322a2cc85bddffa57f07a
diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/refs/pull/2/head b/integrations/gitea-repositories-meta/user2/repo1.git/refs/pull/2/head
new file mode 100644
index 00000000000..98593d65370
--- /dev/null
+++ b/integrations/gitea-repositories-meta/user2/repo1.git/refs/pull/2/head
@@ -0,0 +1 @@
+4a357436d925b5c974181ff12a994538ddc5a269
diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/refs/pull/5/head b/integrations/gitea-repositories-meta/user2/repo1.git/refs/pull/5/head
new file mode 100644
index 00000000000..e0ee44dd145
--- /dev/null
+++ b/integrations/gitea-repositories-meta/user2/repo1.git/refs/pull/5/head
@@ -0,0 +1 @@
+62fb502a7172d4453f0322a2cc85bddffa57f07a
diff --git a/integrations/pull_update_test.go b/integrations/pull_update_test.go
new file mode 100644
index 00000000000..484390001c8
--- /dev/null
+++ b/integrations/pull_update_test.go
@@ -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
+}
diff --git a/integrations/repo_activity_test.go b/integrations/repo_activity_test.go
index cec5c79c4d2..e21f27893dc 100644
--- a/integrations/repo_activity_test.go
+++ b/integrations/repo_activity_test.go
@@ -56,9 +56,9 @@ func TestRepoActivity(t *testing.T) {
list = htmlDoc.doc.Find("#merged-pull-requests").Next().Find("p.desc")
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")
- assert.Len(t, list.Nodes, 2)
+ assert.Len(t, list.Nodes, 3)
// Should be 3 new issues
list = htmlDoc.doc.Find("#new-issues").Next().Find("p.desc")
diff --git a/models/fixtures/issue.yml b/models/fixtures/issue.yml
index ecee7499f6f..e52a23a46b4 100644
--- a/models/fixtures/issue.yml
+++ b/models/fixtures/issue.yml
@@ -122,3 +122,15 @@
created_unix: 946684830
updated_unix: 999307200
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
diff --git a/models/fixtures/pull_request.yml b/models/fixtures/pull_request.yml
index e8d81a0007a..da9566bc481 100644
--- a/models/fixtures/pull_request.yml
+++ b/models/fixtures/pull_request.yml
@@ -49,4 +49,17 @@
head_branch: branch1
base_branch: master
merge_base: abcdef1234567890
- has_merged: false
\ No newline at end of file
+ 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
diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml
index a68e63e309e..05989d90309 100644
--- a/models/fixtures/repository.yml
+++ b/models/fixtures/repository.yml
@@ -7,7 +7,7 @@
is_private: false
num_issues: 2
num_closed_issues: 1
- num_pulls: 2
+ num_pulls: 3
num_closed_pulls: 0
num_milestones: 3
num_closed_milestones: 1
diff --git a/models/issue_test.go b/models/issue_test.go
index ec4867d075f..d65345a508a 100644
--- a/models/issue_test.go
+++ b/models/issue_test.go
@@ -276,8 +276,8 @@ func TestIssue_SearchIssueIDsByKeyword(t *testing.T) {
total, ids, err = SearchIssueIDsByKeyword("for", []int64{1}, 10, 0)
assert.NoError(t, err)
- assert.EqualValues(t, 4, total)
- assert.EqualValues(t, []int64{1, 2, 3, 5}, ids)
+ assert.EqualValues(t, 5, total)
+ assert.EqualValues(t, []int64{1, 2, 3, 5, 11}, ids)
// issue1's comment id 2
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.EqualValues(t, issue.Title, newIssue.Title)
assert.EqualValues(t, issue.Content, newIssue.Content)
- // there are 4 issues and max index is 4 on repository 1, so this one should 5
- assert.EqualValues(t, 5, newIssue.Index)
+ // there are 5 issues and max index is 5 on repository 1, so this one should 6
+ assert.EqualValues(t, 6, newIssue.Index)
_, err = x.ID(issue.ID).Delete(new(Issue))
assert.NoError(t, err)
diff --git a/models/issue_user_test.go b/models/issue_user_test.go
index a57ab33f9ec..01e0bdc6444 100644
--- a/models/issue_user_test.go
+++ b/models/issue_user_test.go
@@ -17,7 +17,7 @@ func Test_newIssueUsers(t *testing.T) {
newIssue := &Issue{
RepoID: repo.ID,
PosterID: 4,
- Index: 5,
+ Index: 6,
Title: "newTestIssueTitle",
Content: "newTestIssueContent",
}
diff --git a/models/pull.go b/models/pull.go
index 1edd890035e..fcfcd221c45 100644
--- a/models/pull.go
+++ b/models/pull.go
@@ -742,3 +742,8 @@ func (pr *PullRequest) IsHeadEqualWithBranch(branchName string) (bool, error) {
}
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
+}
diff --git a/models/pull_test.go b/models/pull_test.go
index 9c27b603aa5..6ceeae66537 100644
--- a/models/pull_test.go
+++ b/models/pull_test.go
@@ -61,10 +61,11 @@ func TestPullRequestsNewest(t *testing.T) {
Labels: []string{},
})
assert.NoError(t, err)
- assert.Equal(t, int64(2), count)
- if assert.Len(t, prs, 2) {
- assert.Equal(t, int64(2), prs[0].ID)
- assert.Equal(t, int64(1), prs[1].ID)
+ assert.EqualValues(t, 3, count)
+ if assert.Len(t, prs, 3) {
+ assert.EqualValues(t, 5, prs[0].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{},
})
assert.NoError(t, err)
- assert.Equal(t, int64(2), count)
- if assert.Len(t, prs, 2) {
- assert.Equal(t, int64(1), prs[0].ID)
- assert.Equal(t, int64(2), prs[1].ID)
+ assert.EqualValues(t, 3, count)
+ if assert.Len(t, prs, 3) {
+ assert.EqualValues(t, 1, prs[0].ID)
+ assert.EqualValues(t, 2, prs[1].ID)
+ assert.EqualValues(t, 5, prs[2].ID)
}
}
diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go
index 4028a6c8b51..8a54c200ff2 100644
--- a/modules/indexer/issues/indexer_test.go
+++ b/modules/indexer/issues/indexer_test.go
@@ -65,7 +65,7 @@ func TestBleveSearchIssues(t *testing.T) {
ids, err = SearchIssuesByKeyword([]int64{1}, "for")
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")
assert.NoError(t, err)
@@ -89,7 +89,7 @@ func TestDBSearchIssues(t *testing.T) {
ids, err = SearchIssuesByKeyword([]int64{1}, "for")
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")
assert.NoError(t, err)
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 60df796e07d..9a4f0535e8c 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -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_checks_success = All checks were successful
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.open_tab = %d Open
diff --git a/routers/repo/pull.go b/routers/repo/pull.go
index 901ab48856f..fc0012ffbe5 100644
--- a/routers/repo/pull.go
+++ b/routers/repo/pull.go
@@ -14,6 +14,7 @@ import (
"net/http"
"path"
"strings"
+ "time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth"
@@ -342,8 +343,21 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare
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 {
- ctx.ServerError("GetLatestCommitStatus", err)
+ ctx.ServerError("LoadProtectedBranch", err)
return nil
}
ctx.Data["EnableStatusCheck"] = pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck
@@ -587,6 +601,72 @@ func ViewPullFiles(ctx *context.Context) {
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", "
", -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
func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) {
issue := checkPullInfo(ctx)
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index 58a2da82fcc..7e81f55de60 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -855,6 +855,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get(".patch", repo.DownloadPullPatch)
m.Get("/commits", context.RepoRef(), repo.ViewPullCommits)
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.Group("/files", func() {
m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.ViewPullFiles)
diff --git a/services/pull/merge.go b/services/pull/merge.go
index b423c663ea6..26c9ab3d1cf 100644
--- a/services/pull/merge.go
+++ b/services/pull/merge.go
@@ -33,11 +33,6 @@ import (
// 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.
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 {
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, "", "")
}()
+ 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.
tmpBasePath, err := createTemporaryRepo(pr)
if err != nil {
@@ -337,46 +387,6 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
outbuf.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
}
diff --git a/services/pull/update.go b/services/pull/update.go
new file mode 100644
index 00000000000..5f055827e1a
--- /dev/null
+++ b/services/pull/update.go
@@ -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
+}
diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl
index f8a82f1a0fc..d15237137d4 100644
--- a/templates/repo/issue/view_content/pull.tmpl
+++ b/templates/repo/issue/view_content/pull.tmpl
@@ -157,6 +157,26 @@
{{$.i18n.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason) }}
{{end}}
+ {{if and .Divergence (gt .Divergence.Behind 0)}}
+