]> source.dussan.org Git - gitea.git/commitdiff
Add option to update pull request by `rebase` (#16125)
authora1012112796 <1012112796@qq.com>
Tue, 31 Aug 2021 14:03:45 +0000 (22:03 +0800)
committerGitHub <noreply@github.com>
Tue, 31 Aug 2021 14:03:45 +0000 (16:03 +0200)
* add option to update pull request by `rebase`

Signed-off-by: a1012112796 <1012112796@qq.com>
integrations/pull_update_test.go
models/pull.go
options/locale/locale_en-US.ini
routers/api/v1/repo/pull.go
routers/web/repo/pull.go
services/pull/merge.go
services/pull/update.go
templates/repo/issue/view_content/pull.tmpl
templates/swagger/v1_json.tmpl
web_src/js/index.js

index 46a1e04b25b87675a0c88a11e9fa546b979953e8..3e3b987b643f18eb6f938db8b5e4dad19f54275d 100644 (file)
@@ -47,6 +47,34 @@ func TestAPIPullUpdate(t *testing.T) {
        })
 }
 
+func TestAPIPullUpdateByRebase(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)
+               assert.NoError(t, pr.LoadBaseRepo())
+               assert.NoError(t, pr.LoadIssue())
+
+               session := loginUser(t, "user2")
+               token := getTokenForLoggedInUser(t, session)
+               req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase&token="+token, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index)
+               session.MakeRequest(t, req, http.StatusOK)
+
+               //Test GetDiverging after update
+               diffCount, err = pull_service.GetDiverging(pr)
+               assert.NoError(t, err)
+               assert.EqualValues(t, 0, diffCount.Behind)
+               assert.EqualValues(t, 1, 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",
index e8e815385d64096058e7a4bb553132bcd3270dad..ea8f2899932f21a25a0d8298f88ed7a3e338f2d8 100644 (file)
@@ -373,6 +373,8 @@ const (
        MergeStyleSquash MergeStyle = "squash"
        // MergeStyleManuallyMerged pr has been merged manually, just mark it as merged directly
        MergeStyleManuallyMerged MergeStyle = "manually-merged"
+       // MergeStyleRebaseUpdate not a merge style, used to update pull head by rebase
+       MergeStyleRebaseUpdate MergeStyle = "rebase-update-only"
 )
 
 // SetMerged sets a pull request to merged and closes the corresponding issue
index 76536f2d497523783ab2bce59f38d0d9419b1a06..3462a15cca4d67db713e83af345d8b2609eb3d01 100644 (file)
@@ -1468,7 +1468,8 @@ pulls.status_checks_failure = Some checks failed
 pulls.status_checks_error = Some checks reported errors
 pulls.status_checks_requested = Required
 pulls.status_checks_details = Details
-pulls.update_branch = Update branch
+pulls.update_branch = Update branch by merge
+pulls.update_branch_rebase = Update branch by rebase
 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
index 0a903101c75362475fd76fc3e00d11470ccda47f..e493e720fb2e535463b9bafaed72f89978ea8a84 100644 (file)
@@ -1065,6 +1065,11 @@ func UpdatePullRequest(ctx *context.APIContext) {
        //   type: integer
        //   format: int64
        //   required: true
+       // - name: style
+       //   in: query
+       //   description: how to update pull request
+       //   type: string
+       //   enum: [merge, rebase]
        // responses:
        //   "200":
        //     "$ref": "#/responses/empty"
@@ -1111,13 +1116,15 @@ func UpdatePullRequest(ctx *context.APIContext) {
                return
        }
 
-       allowedUpdate, err := pull_service.IsUserAllowedToUpdate(pr, ctx.User)
+       rebase := ctx.FormString("style") == "rebase"
+
+       allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(pr, ctx.User)
        if err != nil {
                ctx.Error(http.StatusInternalServerError, "IsUserAllowedToMerge", err)
                return
        }
 
-       if !allowedUpdate {
+       if (!allowedUpdateByMerge && !rebase) || (rebase && !allowedUpdateByRebase) {
                ctx.Status(http.StatusForbidden)
                return
        }
@@ -1125,7 +1132,7 @@ func UpdatePullRequest(ctx *context.APIContext) {
        // default merge commit message
        message := fmt.Sprintf("Merge branch '%s' into %s", pr.BaseBranch, pr.HeadBranch)
 
-       if err = pull_service.Update(pr, ctx.User, message); err != nil {
+       if err = pull_service.Update(pr, ctx.User, message, rebase); err != nil {
                if models.IsErrMergeConflicts(err) {
                        ctx.Error(http.StatusConflict, "Update", "merge failed because of conflict")
                        return
index 8ff2ddf3949d1e1d9cff1f5b31e6550cdc20a85b..885ac3391a7e3711531f9724975d765c622be3cd 100644 (file)
@@ -450,7 +450,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare
        }
 
        if headBranchExist {
-               ctx.Data["UpdateAllowed"], err = pull_service.IsUserAllowedToUpdate(pull, ctx.User)
+               ctx.Data["UpdateAllowed"], ctx.Data["UpdateByRebaseAllowed"], err = pull_service.IsUserAllowedToUpdate(pull, ctx.User)
                if err != nil {
                        ctx.ServerError("IsUserAllowedToUpdate", err)
                        return nil
@@ -724,6 +724,8 @@ func UpdatePullRequest(ctx *context.Context) {
                return
        }
 
+       rebase := ctx.FormString("style") == "rebase"
+
        if err := issue.PullRequest.LoadBaseRepo(); err != nil {
                ctx.ServerError("LoadBaseRepo", err)
                return
@@ -733,14 +735,14 @@ func UpdatePullRequest(ctx *context.Context) {
                return
        }
 
-       allowedUpdate, err := pull_service.IsUserAllowedToUpdate(issue.PullRequest, ctx.User)
+       allowedUpdateByMerge, allowedUpdateByRebase, 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 {
+       if (!allowedUpdateByMerge && !rebase) || (rebase && !allowedUpdateByRebase) {
                ctx.Flash.Error(ctx.Tr("repo.pulls.update_not_allowed"))
                ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
                return
@@ -749,7 +751,7 @@ func UpdatePullRequest(ctx *context.Context) {
        // 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 {
+       if err = pull_service.Update(issue.PullRequest, ctx.User, message, rebase); err != nil {
                if models.IsErrMergeConflicts(err) {
                        conflictError := err.(models.ErrMergeConflicts)
                        flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
index 7e6a214b87d9256a5623f3d1a7bff3c7957c58a2..ef797e1ca477b02f35ef97dfd3fe62933f1000eb 100644 (file)
@@ -253,6 +253,8 @@ func rawMerge(pr *models.PullRequest, doer *models.User, mergeStyle models.Merge
                }
        case models.MergeStyleRebase:
                fallthrough
+       case models.MergeStyleRebaseUpdate:
+               fallthrough
        case models.MergeStyleRebaseMerge:
                // Checkout head branch
                if err := git.NewCommand("checkout", "-b", stagingBranch, trackingBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil {
@@ -305,6 +307,11 @@ func rawMerge(pr *models.PullRequest, doer *models.User, mergeStyle models.Merge
                outbuf.Reset()
                errbuf.Reset()
 
+               // not need merge, just update by rebase. so skip
+               if mergeStyle == models.MergeStyleRebaseUpdate {
+                       break
+               }
+
                // Checkout base branch again
                if err := git.NewCommand("checkout", baseBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil {
                        log.Error("git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
@@ -410,8 +417,16 @@ func rawMerge(pr *models.PullRequest, doer *models.User, mergeStyle models.Merge
                pr.ID,
        )
 
+       var pushCmd *git.Command
+       if mergeStyle == models.MergeStyleRebaseUpdate {
+               // force push the rebase result to head brach
+               pushCmd = git.NewCommand("push", "-f", "head_repo", stagingBranch+":refs/heads/"+pr.HeadBranch)
+       } else {
+               pushCmd = git.NewCommand("push", "origin", baseBranch+":refs/heads/"+pr.BaseBranch)
+       }
+
        // Push back to upstream.
-       if err := git.NewCommand("push", "origin", baseBranch+":refs/heads/"+pr.BaseBranch).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, &outbuf, &errbuf); err != nil {
+       if err := pushCmd.RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, &outbuf, &errbuf); err != nil {
                if strings.Contains(errbuf.String(), "non-fast-forward") {
                        return "", &git.ErrPushOutOfDate{
                                StdOut: outbuf.String(),
index c2c13845e308ae770d94ab9ddd266a6f4764a587..4b4e67797ef3433ca16995e8e17430e57763f98f 100644 (file)
@@ -13,13 +13,24 @@ import (
 )
 
 // 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,
+func Update(pull *models.PullRequest, doer *models.User, message string, rebase bool) error {
+       var (
+               pr    *models.PullRequest
+               style models.MergeStyle
+       )
+
+       if rebase {
+               pr = pull
+               style = models.MergeStyleRebaseUpdate
+       } else {
+               //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,
+               }
+               style = models.MergeStyleMerge
        }
 
        if pull.Flow == models.PullRequestFlowAGit {
@@ -42,9 +53,13 @@ func Update(pull *models.PullRequest, doer *models.User, message string) error {
                return fmt.Errorf("HeadBranch of PR %d is up to date", pull.Index)
        }
 
-       _, err = rawMerge(pr, doer, models.MergeStyleMerge, message)
+       _, err = rawMerge(pr, doer, style, message)
 
        defer func() {
+               if rebase {
+                       go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "")
+                       return
+               }
                go AddTestPullRequestTask(doer, pr.HeadRepo.ID, pr.HeadBranch, false, "", "")
        }()
 
@@ -52,17 +67,17 @@ func Update(pull *models.PullRequest, doer *models.User, message string) error {
 }
 
 // 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) {
+func IsUserAllowedToUpdate(pull *models.PullRequest, user *models.User) (mergeAllowed, rebaseAllowed bool, err error) {
        if pull.Flow == models.PullRequestFlowAGit {
-               return false, nil
+               return false, false, nil
        }
 
        if user == nil {
-               return false, nil
+               return false, false, nil
        }
        headRepoPerm, err := models.GetUserRepoPermission(pull.HeadRepo, user)
        if err != nil {
-               return false, err
+               return false, false, err
        }
 
        pr := &models.PullRequest{
@@ -74,15 +89,25 @@ func IsUserAllowedToUpdate(pull *models.PullRequest, user *models.User) (bool, e
 
        err = pr.LoadProtectedBranch()
        if err != nil {
-               return false, err
+               return false, false, err
+       }
+
+       // can't do rebase on protected branch because need force push
+       if pr.ProtectedBranch == nil {
+               rebaseAllowed = true
        }
 
        // Update function need push permission
        if pr.ProtectedBranch != nil && !pr.ProtectedBranch.CanUserPush(user.ID) {
-               return false, nil
+               return false, false, nil
+       }
+
+       mergeAllowed, err = IsUserAllowedToMerge(pr, headRepoPerm, user)
+       if err != nil {
+               return false, false, err
        }
 
-       return IsUserAllowedToMerge(pr, headRepoPerm, user)
+       return mergeAllowed, rebaseAllowed, nil
 }
 
 // GetDiverging determines how many commits a PR is ahead or behind the PR base branch
index 15ce63b4d37b34df405e315a8aa58ac2b368bcd7..2e624a9fbfb43efb49cc736a7a41581cec9e698d 100644 (file)
                                                        {{$.i18n.Tr "repo.pulls.outdated_with_base_branch"}}
                                                </div>
                                                <div class="item-section-right">
-                                                       {{if .UpdateAllowed}}
+                                                       {{if and .UpdateAllowed .UpdateByRebaseAllowed }}
+                                                               <div class="dib">
+                                                                       <div class="ui buttons update-button">
+                                                                               <button class="ui button" data-do="{{.Link}}/update" data-redirect="{{.Link}}">
+                                                                                       <span class="button-text">
+                                                                                               {{$.i18n.Tr "repo.pulls.update_branch"}}
+                                                                                       </span>
+                                                                               </button>
+
+                                                                               <div class="ui dropdown icon button no-text">
+                                                                                       {{svg "octicon-triangle-down" 14 "dropdown icon"}}
+                                                                                       <div class="menu">
+                                                                                               <div class="item active selected" data-do="{{.Link}}/update">{{$.i18n.Tr "repo.pulls.update_branch"}}</div>
+                                                                                               <div class="item" data-do="{{.Link}}/update?style=rebase">{{$.i18n.Tr "repo.pulls.update_branch_rebase"}}</div>
+                                                                                       </div>
+                                                                               </div>
+                                                                       </div>
+                                                               </div>
+                                                       {{end}}
+                                                       {{if and .UpdateAllowed (not .UpdateByRebaseAllowed)}}
                                                                <form action="{{.Link}}/update" method="post" class="ui update-branch-form">
                                                                        {{.CsrfTokenHtml}}
                                                                        <button class="ui compact button" data-do="update">
                                                {{$.i18n.Tr "repo.pulls.outdated_with_base_branch"}}
                                        </div>
                                        <div>
-                                               {{if .UpdateAllowed}}
+                                               {{if and .UpdateAllowed .UpdateByRebaseAllowed }}
+                                                       <div class="dib">
+                                                               <div class="ui buttons update-button">
+                                                                       <button class="ui button" data-do="{{.Link}}/update" data-redirect="{{.Link}}">
+                                                                               <span class="button-text">
+                                                                                       {{$.i18n.Tr "repo.pulls.update_branch"}}
+                                                                               </span>
+                                                                       </button>
+
+                                                                       <div class="ui dropdown icon button no-text">
+                                                                               {{svg "octicon-triangle-down" 14 "dropdown icon"}}
+                                                                               <div class="menu">
+                                                                                       <div class="item active selected" data-do="{{.Link}}/update">{{$.i18n.Tr "repo.pulls.update_branch"}}</div>
+                                                                                       <div class="item" data-do="{{.Link}}/update?style=rebase">{{$.i18n.Tr "repo.pulls.update_branch_rebase"}}</div>
+                                                                               </div>
+                                                                       </div>
+                                                               </div>
+                                                       </div>
+                                               {{end}}
+                                               {{if and .UpdateAllowed (not .UpdateByRebaseAllowed)}}
                                                        <form action="{{.Link}}/update" method="post">
                                                                {{.CsrfTokenHtml}}
                                                                <button class="ui compact button" data-do="update">
index ee759ba6b171a991a8bceb288e833d65ce278bd6..90a32e28aff45667268e16c45c16f3d615303ec8 100644 (file)
             "name": "index",
             "in": "path",
             "required": true
+          },
+          {
+            "enum": [
+              "merge",
+              "rebase"
+            ],
+            "type": "string",
+            "description": "how to update pull request",
+            "name": "style",
+            "in": "query"
           }
         ],
         "responses": {
index 7e4970c3a062b8558923189196c080d953e01b4b..ba025cfadee78e76caf295bfc88168a868bdeb8c 100644 (file)
@@ -1267,6 +1267,36 @@ async function initRepository() {
       $('.instruct-toggle').show();
     });
 
+    // Pull Request update button
+    const $pullUpdateButton = $('.update-button > button');
+    $pullUpdateButton.on('click', function (e) {
+      e.preventDefault();
+      const $this = $(this);
+      const redirect = $this.data('redirect');
+      $this.addClass('loading');
+      $.post($this.data('do'), {
+        _csrf: csrf
+      }).done((data) => {
+        if (data.redirect) {
+          window.location.href = data.redirect;
+        } else if (redirect) {
+          window.location.href = redirect;
+        } else {
+          window.location.reload();
+        }
+      });
+    });
+
+    $('.update-button > .dropdown').dropdown({
+      onChange(_text, _value, $choice) {
+        const $url = $choice.data('do');
+        if ($url) {
+          $pullUpdateButton.find('.button-text').text($choice.text());
+          $pullUpdateButton.data('do', $url);
+        }
+      }
+    });
+
     initReactionSelector();
   }