]> source.dussan.org Git - gitea.git/commitdiff
Add "Update Branch" button to Pull Requests (#9784)
author6543 <6543@obermui.de>
Fri, 17 Jan 2020 06:03:40 +0000 (07:03 +0100)
committerLauris BH <lauris@nix.lv>
Fri, 17 Jan 2020 06:03:40 +0000 (08:03 +0200)
* 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>
32 files changed:
integrations/api_issue_test.go
integrations/gitea-repositories-meta/user2/repo1.git/info/refs
integrations/gitea-repositories-meta/user2/repo1.git/objects/5c/050d3b6d2db231ab1f64e324f1b6b9a0b181c2 [new file with mode: 0644]
integrations/gitea-repositories-meta/user2/repo1.git/objects/62/fb502a7172d4453f0322a2cc85bddffa57f07a [new file with mode: 0644]
integrations/gitea-repositories-meta/user2/repo1.git/objects/6a/a3a5385611c5eb8986c9961a9c34a93cbaadfb [new file with mode: 0644]
integrations/gitea-repositories-meta/user2/repo1.git/objects/7c/4df115542e05c700f297519e906fd63c9c9804 [new file with mode: 0644]
integrations/gitea-repositories-meta/user2/repo1.git/objects/94/922e1295c678267de1193b7b84ad8a086c27f9 [new file with mode: 0644]
integrations/gitea-repositories-meta/user2/repo1.git/objects/98/5f0301dba5e7b34be866819cd15ad3d8f508ee [new file with mode: 0644]
integrations/gitea-repositories-meta/user2/repo1.git/objects/a6/9277c81e90b98a7c0ab25b042a6e296da8eb9a [new file with mode: 0644]
integrations/gitea-repositories-meta/user2/repo1.git/objects/a7/57c0ea621e63d0fd6fc353a175fdc7199e5d1d [new file with mode: 0644]
integrations/gitea-repositories-meta/user2/repo1.git/objects/b2/60587271671842af0b036e4fe643c9d45b7ddd [new file with mode: 0644]
integrations/gitea-repositories-meta/user2/repo1.git/refs/heads/branch2 [new file with mode: 0644]
integrations/gitea-repositories-meta/user2/repo1.git/refs/heads/pr-to-update [new file with mode: 0644]
integrations/gitea-repositories-meta/user2/repo1.git/refs/pull/2/head [new file with mode: 0644]
integrations/gitea-repositories-meta/user2/repo1.git/refs/pull/5/head [new file with mode: 0644]
integrations/pull_update_test.go [new file with mode: 0644]
integrations/repo_activity_test.go
models/fixtures/issue.yml
models/fixtures/pull_request.yml
models/fixtures/repository.yml
models/issue_test.go
models/issue_user_test.go
models/pull.go
models/pull_test.go
modules/indexer/issues/indexer_test.go
options/locale/locale_en-US.ini
routers/repo/pull.go
routers/routes/routes.go
services/pull/merge.go
services/pull/update.go [new file with mode: 0644]
templates/repo/issue/view_content/pull.tmpl
web_src/less/_repository.less

index ce1c4b7d33fcbc2ad2ada1e31869a7c385610287..906dbb2dc7ed76499c864afe342233b32fbf24b3 100644 (file)
@@ -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)
 }
index ca1df85e2ebfb4ea2dd2e6d88107dda97a4c8644..fa3009793de7047f277cbab8d94e6f0280990bd5 100644 (file)
@@ -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 (file)
index 0000000..c0cb626
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 (file)
index 0000000..ee494a8
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 (file)
index 0000000..09aed94
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 (file)
index 0000000..3bf67a2
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 (file)
index 0000000..60692df
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 (file)
index 0000000..81fd6a5
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 (file)
index 0000000..8876698
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 (file)
index 0000000..c3111a0
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 (file)
index 0000000..9182ac0
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 (file)
index 0000000..5add725
--- /dev/null
@@ -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 (file)
index 0000000..e0ee44d
--- /dev/null
@@ -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 (file)
index 0000000..98593d6
--- /dev/null
@@ -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 (file)
index 0000000..e0ee44d
--- /dev/null
@@ -0,0 +1 @@
+62fb502a7172d4453f0322a2cc85bddffa57f07a
diff --git a/integrations/pull_update_test.go b/integrations/pull_update_test.go
new file mode 100644 (file)
index 0000000..4843900
--- /dev/null
@@ -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
+}
index cec5c79c4d200b974ecbc87a4646ff5823484df0..e21f27893dc947ae3287a82b8cdbee49ca77a2b8 100644 (file)
@@ -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")
index ecee7499f6f047e07509225ddc53f3a269bf7a0b..e52a23a46b4eb12196d0febdd60db7b20be3a022 100644 (file)
   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
index e8d81a0007aa599f0f864f00d85b87d6c08b6b8a..da9566bc481eb08f771b6d5b760b872b0ec32f45 100644 (file)
   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
index a68e63e309ee0b653fa84a356c463a2ef07caa8c..05989d90309872ed576b392ca61c354d36da64a0 100644 (file)
@@ -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
index ec4867d075f7e28b12e21303d3dd0c0e247fd627..d65345a508a1ac4b4ba88215ab790423652b6c2b 100644 (file)
@@ -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)
index a57ab33f9ec1a0f4469960e41ee7069b6e41e73d..01e0bdc6444f07099516f1ecc4dc4589bd89763c 100644 (file)
@@ -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",
        }
index 1edd890035e39ea38626747fddc787393ac0efa2..fcfcd221c459b1c5d458e99a10295a5a90b206d1 100644 (file)
@@ -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
+}
index 9c27b603aa534588eeba7e96b4dcfb48a8bdc6f2..6ceeae6653769d397562b8fae408981db4339576 100644 (file)
@@ -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)
        }
 }
 
index 4028a6c8b518d541d2b89ff1c4c5256dc52eee94..8a54c200ff2ee4b9ae0d0e0b66a16a91d9692b5d 100644 (file)
@@ -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)
index 60df796e07d0373f79312999f47d35e4a05c77fd..9a4f0535e8c676219fef94b91b47fe3778f0e881 100644 (file)
@@ -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
index 901ab48856f358ec59dcb25469aa3b315af3cf4a..fc0012ffbe559025cc3d68f41781513843c56de1 100644 (file)
@@ -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", "<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
 func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) {
        issue := checkPullInfo(ctx)
index 58a2da82fccaaa26f3830b157f2650dea5c49b2b..7e81f55de60faec165eb087df5e6e3f8579028d3 100644 (file)
@@ -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)
index b423c663ea6db5ef9b3ec4ec6cf4226dbebbe750..26c9ab3d1cfae56145331391e13289f025d3463e 100644 (file)
@@ -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 (file)
index 0000000..5f05582
--- /dev/null
@@ -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
+}
index f8a82f1a0fc1e61bbf5415e1255c13d4c9d3f899..d15237137d4fdf978b17a67496f5586ccefdbce5 100644 (file)
                                                        {{$.i18n.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason) }}
                                                </div>
                                        {{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}}
                                                {{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}}
                                                {{$approvers := .Issue.PullRequest.GetApprovers}}
index 27a0698f7b6c83ac9cc74223491adde58040e9c6..a1b55e86aaf6014257e703881c0cabf16b37606c 100644 (file)
                 .icon-octicon {
                     padding-left: 2px;
                 }
+                .branch-update.grid {
+                    margin-bottom: -1.5rem;
+                    margin-top: -0.5rem;
+                    .row {
+                        padding-bottom: 0;
+                    }
+                }
             }
 
             .review-item {