]> source.dussan.org Git - gitea.git/commitdiff
Make manual merge autodetection optional and add manual merge as merge method (#12543)
authora1012112796 <1012112796@qq.com>
Thu, 4 Mar 2021 03:41:23 +0000 (11:41 +0800)
committerGitHub <noreply@github.com>
Thu, 4 Mar 2021 03:41:23 +0000 (22:41 -0500)
* Make auto check manual merge as a chooseable mod and add manual merge way on ui

as title, Before this pr, we use same way with GH to check manually merge.
It good, but in some special cases, misjudgments can occur. and it's hard
to fix this bug. So I add option to allow repo manager block "auto check manual merge"
function, Then it will have same style like gitlab(allow empty pr). and to compensate for
not being able to detect THE PR merge automatically, I added a manual approach.

Signed-off-by: a1012112796 <1012112796@qq.com>
* make swager

* api support

* ping ci

* fix TestPullCreate_EmptyChangesWithCommits

* Apply suggestions from code review

Co-authored-by: zeripath <art27@cantab.net>
* Apply review suggestions and add test

* Apply suggestions from code review

Co-authored-by: zeripath <art27@cantab.net>
* fix build

* test error message

* make fmt

* Fix indentation issues identified by @silverwind

Co-authored-by: silverwind <me@silverwind.io>
* Fix tests and make manually merged disabled error on API the same

Signed-off-by: Andrew Thornton <art27@cantab.net>
* a small nit

* fix wrong commit id error

* fix bug

* simple test

* fix test

Co-authored-by: zeripath <art27@cantab.net>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: techknowlogick <techknowlogick@gitea.io>
25 files changed:
integrations/api_helper_for_declarative_test.go
integrations/git_test.go
integrations/pull_status_test.go
models/pull.go
models/repo_unit.go
modules/forms/repo_form.go
modules/git/repo_commit.go
modules/git/repo_commit_test.go
modules/structs/repo.go
options/locale/locale_en-US.ini
routers/api/v1/repo/pull.go
routers/api/v1/repo/repo.go
routers/repo/compare.go
routers/repo/issue.go
routers/repo/pull.go
routers/repo/setting.go
services/pull/check.go
services/pull/merge.go
services/pull/patch.go
templates/repo/diff/compare.tmpl
templates/repo/issue/view_content/comments.tmpl
templates/repo/issue/view_content/pull.tmpl
templates/repo/issue/view_title.tmpl
templates/repo/settings/options.tmpl
templates/swagger/v1_json.tmpl

index f1d57e717cac35a30c610234f8dcf88f5b5cbda4..913fce1577bd1b7869e84da6f83130724027dbda 100644 (file)
@@ -9,6 +9,7 @@ import (
        "fmt"
        "io/ioutil"
        "net/http"
+       "net/url"
        "testing"
        "time"
 
@@ -71,6 +72,23 @@ func doAPICreateRepository(ctx APITestContext, empty bool, callback ...func(*tes
        }
 }
 
+func doAPIEditRepository(ctx APITestContext, editRepoOption *api.EditRepoOption, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
+       return func(t *testing.T) {
+               req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), ctx.Token), editRepoOption)
+               if ctx.ExpectedCode != 0 {
+                       ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+                       return
+               }
+               resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
+
+               var repository api.Repository
+               DecodeJSON(t, resp, &repository)
+               if len(callback) > 0 {
+                       callback[0](t, repository)
+               }
+       }
+}
+
 func doAPIAddCollaborator(ctx APITestContext, username string, mode models.AccessMode) func(*testing.T) {
        return func(t *testing.T) {
                permission := "read"
@@ -256,6 +274,23 @@ func doAPIMergePullRequest(ctx APITestContext, owner, repo string, index int64)
        }
 }
 
+func doAPIManuallyMergePullRequest(ctx APITestContext, owner, repo, commitID string, index int64) func(*testing.T) {
+       return func(t *testing.T) {
+               urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge?token=%s",
+                       owner, repo, index, ctx.Token)
+               req := NewRequestWithJSON(t, http.MethodPost, urlStr, &auth.MergePullRequestForm{
+                       Do:            string(models.MergeStyleManuallyMerged),
+                       MergeCommitID: commitID,
+               })
+
+               if ctx.ExpectedCode != 0 {
+                       ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+                       return
+               }
+               ctx.Session.MakeRequest(t, req, 200)
+       }
+}
+
 func doAPIGetBranch(ctx APITestContext, branch string, callback ...func(*testing.T, api.Branch)) func(*testing.T) {
        return func(t *testing.T) {
                req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/branches/%s?token=%s", ctx.Username, ctx.Reponame, branch, ctx.Token)
index c3c11268296f98d58f18928e129f090d7c59ae5a..705bd08c1183c0b500d4a49cfc84b0680f9ab146 100644 (file)
@@ -69,6 +69,7 @@ func testGit(t *testing.T, u *url.URL) {
                mediaTest(t, &httpContext, little, big, littleLFS, bigLFS)
 
                t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath))
+               t.Run("CreatePRAndSetManuallyMerged", doCreatePRAndSetManuallyMerged(httpContext, httpContext, dstPath, "master", "test-manually-merge"))
                t.Run("MergeFork", func(t *testing.T) {
                        defer PrintCurrentTest(t)()
                        t.Run("CreatePRAndMerge", doMergeFork(httpContext, forkedUserCtx, "master", httpContext.Username+":master"))
@@ -468,6 +469,35 @@ func doMergeFork(ctx, baseCtx APITestContext, baseBranch, headBranch string) fun
        }
 }
 
+func doCreatePRAndSetManuallyMerged(ctx, baseCtx APITestContext, dstPath, baseBranch, headBranch string) func(t *testing.T) {
+       return func(t *testing.T) {
+               defer PrintCurrentTest(t)()
+               var (
+                       pr           api.PullRequest
+                       err          error
+                       lastCommitID string
+               )
+
+               trueBool := true
+               falseBool := false
+
+               t.Run("AllowSetManuallyMergedAndSwitchOffAutodetectManualMerge", doAPIEditRepository(baseCtx, &api.EditRepoOption{
+                       HasPullRequests:       &trueBool,
+                       AllowManualMerge:      &trueBool,
+                       AutodetectManualMerge: &falseBool,
+               }))
+
+               t.Run("CreateHeadBranch", doGitCreateBranch(dstPath, headBranch))
+               t.Run("PushToHeadBranch", doGitPushTestRepository(dstPath, "origin", headBranch))
+               t.Run("CreateEmptyPullRequest", func(t *testing.T) {
+                       pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, baseBranch, headBranch)(t)
+                       assert.NoError(t, err)
+               })
+               lastCommitID = pr.Base.Sha
+               t.Run("ManuallyMergePR", doAPIManuallyMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, lastCommitID, pr.Index))
+       }
+}
+
 func doEnsureCanSeePull(ctx APITestContext, pr api.PullRequest) func(t *testing.T) {
        return func(t *testing.T) {
                req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index))
index fc4c7ca33da1a72d84c3cd0a2405541a8cda5351..7c6f3d44c5223cfc290fa6cc0d3e570951078a09 100644 (file)
@@ -115,6 +115,6 @@ func TestPullCreate_EmptyChangesWithCommits(t *testing.T) {
                doc := NewHTMLParser(t, resp.Body)
 
                text := strings.TrimSpace(doc.doc.Find(".merge-section").Text())
-               assert.Contains(t, text, "This pull request can be merged automatically.")
+               assert.Contains(t, text, "This branch is equal with the target branch.")
        })
 }
index 0eba65db4fada835fa129576393f0f4d9f01d047..7dacf6a8d79e1976747b99f43f0ce4c0ec2f453e 100644 (file)
@@ -35,6 +35,7 @@ const (
        PullRequestStatusMergeable
        PullRequestStatusManuallyMerged
        PullRequestStatusError
+       PullRequestStatusEmpty
 )
 
 // PullRequest represents relation between pull request and repositories.
@@ -332,6 +333,11 @@ func (pr *PullRequest) CanAutoMerge() bool {
        return pr.Status == PullRequestStatusMergeable
 }
 
+// IsEmpty returns true if this pull request is empty.
+func (pr *PullRequest) IsEmpty() bool {
+       return pr.Status == PullRequestStatusEmpty
+}
+
 // MergeStyle represents the approach to merge commits into base branch.
 type MergeStyle string
 
@@ -344,6 +350,8 @@ const (
        MergeStyleRebaseMerge MergeStyle = "rebase-merge"
        // MergeStyleSquash squash commits into single commit before merging
        MergeStyleSquash MergeStyle = "squash"
+       // MergeStyleManuallyMerged pr has been merged manually, just mark it as merged directly
+       MergeStyleManuallyMerged MergeStyle = "manually-merged"
 )
 
 // SetMerged sets a pull request to merged and closes the corresponding issue
index 3ef390483369583aeef766b5ec95cc462524b432..0feddfe2ea0cba2ceffc4ad1452442afabadb39e 100644 (file)
@@ -101,6 +101,8 @@ type PullRequestsConfig struct {
        AllowRebase               bool
        AllowRebaseMerge          bool
        AllowSquash               bool
+       AllowManualMerge          bool
+       AutodetectManualMerge     bool
 }
 
 // FromDB fills up a PullRequestsConfig from serialized format.
@@ -120,7 +122,8 @@ func (cfg *PullRequestsConfig) IsMergeStyleAllowed(mergeStyle MergeStyle) bool {
        return mergeStyle == MergeStyleMerge && cfg.AllowMerge ||
                mergeStyle == MergeStyleRebase && cfg.AllowRebase ||
                mergeStyle == MergeStyleRebaseMerge && cfg.AllowRebaseMerge ||
-               mergeStyle == MergeStyleSquash && cfg.AllowSquash
+               mergeStyle == MergeStyleSquash && cfg.AllowSquash ||
+               mergeStyle == MergeStyleManuallyMerged && cfg.AllowManualMerge
 }
 
 // AllowedMergeStyleCount returns the total count of allowed merge styles for the PullRequestsConfig
index 2793acdd5b74069cfb7fc647060f2fe400a67f76..ab88aef571f0bb98b397d4e98a75ea4f372ca4cf 100644 (file)
@@ -156,6 +156,8 @@ type RepoSettingForm struct {
        PullsAllowRebase                 bool
        PullsAllowRebaseMerge            bool
        PullsAllowSquash                 bool
+       PullsAllowManualMerge            bool
+       EnableAutodetectManualMerge      bool
        EnableTimetracker                bool
        AllowOnlyContributorsToTrackTime bool
        EnableIssueDependencies          bool
@@ -556,11 +558,12 @@ func (f *InitializeLabelsForm) Validate(req *http.Request, errs binding.Errors)
 // swagger:model MergePullRequestOption
 type MergePullRequestForm struct {
        // required: true
-       // enum: merge,rebase,rebase-merge,squash
-       Do                string `binding:"Required;In(merge,rebase,rebase-merge,squash)"`
+       // enum: merge,rebase,rebase-merge,squash,manually-merged
+       Do                string `binding:"Required;In(merge,rebase,rebase-merge,squash,manually-merged)"`
        MergeTitleField   string
        MergeMessageField string
-       ForceMerge        *bool `json:"force_merge,omitempty"`
+       MergeCommitID     string // only used for manually-merged
+       ForceMerge        *bool  `json:"force_merge,omitempty"`
 }
 
 // Validate validates the fields
index 5bf113ba49d9f99415d969e74897715977e4d135..ea0aeeb35d3701c09d1f35c88ebacf3e478028dd 100644 (file)
@@ -456,3 +456,12 @@ func (repo *Repository) GetCommitsFromIDs(commitIDs []string) (commits *list.Lis
 
        return commits
 }
+
+// IsCommitInBranch check if the commit is on the branch
+func (repo *Repository) IsCommitInBranch(commitID, branch string) (r bool, err error) {
+       stdout, err := NewCommand("branch", "--contains", commitID, branch).RunInDir(repo.Path)
+       if err != nil {
+               return false, err
+       }
+       return len(stdout) > 0, err
+}
index 87dd6763b3944d08255de153e7a2b5c06ff2abd3..3eedaa6b6e9b278e074b66ce704b50c560d64fc5 100644 (file)
@@ -63,3 +63,18 @@ func TestGetCommitWithBadCommitID(t *testing.T) {
        assert.Error(t, err)
        assert.True(t, IsErrNotExist(err))
 }
+
+func TestIsCommitInBranch(t *testing.T) {
+       bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
+       bareRepo1, err := OpenRepository(bareRepo1Path)
+       assert.NoError(t, err)
+       defer bareRepo1.Close()
+
+       result, err := bareRepo1.IsCommitInBranch("2839944139e0de9737a044f78b0e4b40d989a9e3", "branch1")
+       assert.NoError(t, err)
+       assert.Equal(t, true, result)
+
+       result, err = bareRepo1.IsCommitInBranch("2839944139e0de9737a044f78b0e4b40d989a9e3", "branch2")
+       assert.NoError(t, err)
+       assert.Equal(t, false, result)
+}
index d588813b218836b9e4c23a38098920f3a6891e6d..c47700cd009341dc5cdc622758de449f203be749 100644 (file)
@@ -167,6 +167,10 @@ type EditRepoOption struct {
        AllowRebaseMerge *bool `json:"allow_rebase_explicit,omitempty"`
        // either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging. `has_pull_requests` must be `true`.
        AllowSquash *bool `json:"allow_squash_merge,omitempty"`
+       // either `true` to allow mark pr as merged manually, or `false` to prevent it. `has_pull_requests` must be `true`.
+       AllowManualMerge *bool `json:"allow_manual_merge,omitempty"`
+       // either `true` to enable AutodetectManualMerge, or `false` to prevent it. `has_pull_requests` must be `true`, Note: In some special cases, misjudgments can occur.
+       AutodetectManualMerge *bool `json:"autodetect_manual_merge,omitempty"`
        // set to `true` to archive this repository.
        Archived *bool `json:"archived,omitempty"`
        // set to a string like `8h30m0s` to set the mirror interval time
index 0ee8e7ab0c331b6e0006bd5cb7a182f8b59464f9..08b202d192f84cdff4ed812f2a646765dee67a76 100644 (file)
@@ -1105,6 +1105,7 @@ issues.context.delete = Delete
 issues.no_content = There is no content yet.
 issues.close_issue = Close
 issues.pull_merged_at = `merged commit <a href="%[1]s">%[2]s</a> into <b>%[3]s</b> %[4]s`
+issues.manually_pull_merged_at = `merged commit <a href="%[1]s">%[2]s</a> into <b>%[3]s</b> %[4]s manually`
 issues.close_comment_issue = Comment and Close
 issues.reopen_issue = Reopen
 issues.reopen_comment_issue = Comment and Reopen
@@ -1273,6 +1274,7 @@ pulls.compare_compare = pull from
 pulls.filter_branch = Filter branch
 pulls.no_results = No results found.
 pulls.nothing_to_compare = These branches are equal. There is no need to create a pull request.
+pulls.nothing_to_compare_and_allow_empty_pr = These branches are equal. This PR will be empty.
 pulls.has_pull_request = `A pull request between these branches already exists: <a href="%[1]s/pulls/%[3]d">%[2]s#%[3]d</a>`
 pulls.create = Create Pull Request
 pulls.title_desc = wants to merge %[1]d commits from <code>%[2]s</code> into <code id="branch_target">%[3]s</code>
@@ -1285,6 +1287,8 @@ pulls.reopen_to_merge = Please reopen this pull request to perform a merge.
 pulls.cant_reopen_deleted_branch = This pull request cannot be reopened because the branch was deleted.
 pulls.merged = Merged
 pulls.merged_as = The pull request has been merged as <a rel="nofollow" class="ui sha" href="%[1]s"><code>%[2]s</code></a>.
+pulls.manually_merged = Manually merged
+pulls.manually_merged_as = The pull request has been manually merged as <a rel="nofollow" class="ui sha" href="%[1]s"><code>%[2]s</code></a>.
 pulls.is_closed = The pull request has been closed.
 pulls.has_merged = The pull request has been merged.
 pulls.title_wip_desc = `<a href="#">Start the title with <strong>%s</strong></a> to prevent the pull request from being merged accidentally.`
@@ -1292,6 +1296,7 @@ pulls.cannot_merge_work_in_progress = This pull request is marked as a work in p
 pulls.data_broken = This pull request is broken due to missing fork information.
 pulls.files_conflicted = This pull request has changes conflicting with the target branch.
 pulls.is_checking = "Merge conflict checking is in progress. Try again in few moments."
+pulls.is_empty = "This branch is equal with the target branch."
 pulls.required_status_check_failed = Some required checks were not successful.
 pulls.required_status_check_missing = Some required checks are missing.
 pulls.required_status_check_administrator = As an administrator, you may still merge this pull request.
@@ -1312,6 +1317,7 @@ pulls.reject_count_1 = "%d change request"
 pulls.reject_count_n = "%d change requests"
 pulls.waiting_count_1 = "%d waiting review"
 pulls.waiting_count_n = "%d waiting reviews"
+pulls.wrong_commit_id = "commit id must be a commit id on the target branch"
 
 pulls.no_merge_desc = This pull request cannot be merged because all repository merge options are disabled.
 pulls.no_merge_helper = Enable merge options in the repository settings or merge the pull request manually.
@@ -1322,6 +1328,8 @@ pulls.merge_pull_request = Merge Pull Request
 pulls.rebase_merge_pull_request = Rebase and Merge
 pulls.rebase_merge_commit_pull_request = Rebase and Merge (--no-ff)
 pulls.squash_merge_pull_request = Squash and Merge
+pulls.merge_manually = Manually merged
+pulls.merge_commit_id = The merge commit ID
 pulls.require_signed_wont_sign = The branch requires signed commits but this merge will not be signed
 pulls.invalid_merge_option = You cannot use this merge option for this pull request.
 pulls.merge_conflict = Merge Failed: There was a conflict whilst merging. Hint: Try a different strategy
@@ -1545,6 +1553,8 @@ settings.pulls.allow_merge_commits = Enable Commit Merging
 settings.pulls.allow_rebase_merge = Enable Rebasing to Merge Commits
 settings.pulls.allow_rebase_merge_commit = Enable Rebasing with explicit merge commits (--no-ff)
 settings.pulls.allow_squash_commits = Enable Squashing to Merge Commits
+settings.pulls.allow_manual_merge = Enable Mark PR as manually merged
+settings.pulls.enable_autodetect_manual_merge = Enable autodetect manual merge (Note: In some special cases, misjudgments can occur)
 settings.projects_desc = Enable Repository Projects
 settings.admin_settings = Administrator Settings
 settings.admin_enable_health_check = Enable Repository Health Checks (git fsck)
index 38dac365533535d301f66ec025e7b97c07a3b383..8eda9496522156e66f4126465fadd4c543c20652 100644 (file)
@@ -769,13 +769,31 @@ func MergePullRequest(ctx *context.APIContext) {
                return
        }
 
-       if !pr.CanAutoMerge() {
-               ctx.Error(http.StatusMethodNotAllowed, "PR not in mergeable state", "Please try again later")
+       if pr.HasMerged {
+               ctx.Error(http.StatusMethodNotAllowed, "PR already merged", "")
                return
        }
 
-       if pr.HasMerged {
-               ctx.Error(http.StatusMethodNotAllowed, "PR already merged", "")
+       // handle manually-merged mark
+       if models.MergeStyle(form.Do) == models.MergeStyleManuallyMerged {
+               if err = pull_service.MergedManually(pr, ctx.User, ctx.Repo.GitRepo, form.MergeCommitID); err != nil {
+                       if models.IsErrInvalidMergeStyle(err) {
+                               ctx.Error(http.StatusMethodNotAllowed, "Invalid merge style", fmt.Errorf("%s is not allowed an allowed merge style for this repository", models.MergeStyle(form.Do)))
+                               return
+                       }
+                       if strings.Contains(err.Error(), "Wrong commit ID") {
+                               ctx.JSON(http.StatusConflict, err)
+                               return
+                       }
+                       ctx.Error(http.StatusInternalServerError, "Manually-Merged", err)
+                       return
+               }
+               ctx.Status(http.StatusOK)
+               return
+       }
+
+       if !pr.CanAutoMerge() {
+               ctx.Error(http.StatusMethodNotAllowed, "PR not in mergeable state", "Please try again later")
                return
        }
 
index b84c5993e47922702a04cd3e97c97f850e67759e..b12797f83b990700b61e5e765c824be374a21c81 100644 (file)
@@ -725,6 +725,8 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
                                        AllowRebase:               true,
                                        AllowRebaseMerge:          true,
                                        AllowSquash:               true,
+                                       AllowManualMerge:          true,
+                                       AutodetectManualMerge:     false,
                                }
                        } else {
                                config = unit.PullRequestsConfig()
@@ -745,6 +747,12 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
                        if opts.AllowSquash != nil {
                                config.AllowSquash = *opts.AllowSquash
                        }
+                       if opts.AllowManualMerge != nil {
+                               config.AllowManualMerge = *opts.AllowManualMerge
+                       }
+                       if opts.AutodetectManualMerge != nil {
+                               config.AutodetectManualMerge = *opts.AutodetectManualMerge
+                       }
 
                        units = append(units, models.RepoUnit{
                                RepoID: repo.ID,
index 2eef20f5ff317b1adb3f93fa82ddb43008977cdf..a8f4f8add8d67c6867f940e737f26b82073d7aea 100644 (file)
@@ -429,6 +429,14 @@ func PrepareCompareDiff(
 
        if headCommitID == compareInfo.MergeBase {
                ctx.Data["IsNothingToCompare"] = true
+               if unit, err := repo.GetUnit(models.UnitTypePullRequests); err == nil {
+                       config := unit.PullRequestsConfig()
+                       if !config.AutodetectManualMerge {
+                               ctx.Data["AllowEmptyPr"] = !(baseBranch == headBranch && ctx.Repo.Repository.Name == headRepo.Name)
+                       } else {
+                               ctx.Data["AllowEmptyPr"] = false
+                       }
+               }
                return true
        }
 
index a9459a10ed93dbe2c8ccb282384b807920d95b3e..99df9db18380a418a5586cf3b06383d52c83c304 100644 (file)
@@ -1491,6 +1491,8 @@ func ViewIssue(ctx *context.Context) {
                                ctx.Data["MergeStyle"] = models.MergeStyleRebaseMerge
                        } else if prConfig.AllowSquash {
                                ctx.Data["MergeStyle"] = models.MergeStyleSquash
+                       } else if prConfig.AllowManualMerge {
+                               ctx.Data["MergeStyle"] = models.MergeStyleManuallyMerged
                        } else {
                                ctx.Data["MergeStyle"] = ""
                        }
@@ -1531,6 +1533,22 @@ func ViewIssue(ctx *context.Context) {
                        pull.HeadRepo != nil &&
                        git.IsBranchExist(pull.HeadRepo.RepoPath(), pull.HeadBranch) &&
                        (!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"])
+
+               stillCanManualMerge := func() bool {
+                       if pull.HasMerged || issue.IsClosed || !ctx.IsSigned {
+                               return false
+                       }
+                       if pull.CanAutoMerge() || pull.IsWorkInProgress() || pull.IsChecking() {
+                               return false
+                       }
+                       if (ctx.User.IsAdmin || ctx.Repo.IsAdmin()) && prConfig.AllowManualMerge {
+                               return true
+                       }
+
+                       return false
+               }
+
+               ctx.Data["StillCanManualMerge"] = stillCanManualMerge()
        }
 
        // Get Dependencies
index 15af2a2a3f33bc52a0c45a2ab25bc1faf7156f47..2ed47605f8edad6723be2c897bca9447bbcf3b0b 100644 (file)
@@ -33,6 +33,7 @@ import (
        "code.gitea.io/gitea/services/gitdiff"
        pull_service "code.gitea.io/gitea/services/pull"
        repo_service "code.gitea.io/gitea/services/repository"
+       "github.com/unknwon/com"
 )
 
 const (
@@ -794,15 +795,36 @@ func MergePullRequest(ctx *context.Context) {
                return
        }
 
-       if !pr.CanAutoMerge() {
-               ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_not_ready"))
-               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
+       if pr.HasMerged {
+               ctx.Flash.Error(ctx.Tr("repo.pulls.has_merged"))
+               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
                return
        }
 
-       if pr.HasMerged {
-               ctx.Flash.Error(ctx.Tr("repo.pulls.has_merged"))
-               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
+       // handle manually-merged mark
+       if models.MergeStyle(form.Do) == models.MergeStyleManuallyMerged {
+               if err = pull_service.MergedManually(pr, ctx.User, ctx.Repo.GitRepo, form.MergeCommitID); err != nil {
+                       if models.IsErrInvalidMergeStyle(err) {
+                               ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
+                               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
+                               return
+                       } else if strings.Contains(err.Error(), "Wrong commit ID") {
+                               ctx.Flash.Error(ctx.Tr("repo.pulls.wrong_commit_id"))
+                               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
+                               return
+                       }
+
+                       ctx.ServerError("MergedManually", err)
+                       return
+               }
+
+               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
+               return
+       }
+
+       if !pr.CanAutoMerge() {
+               ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_not_ready"))
+               ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
                return
        }
 
index b35828d7b1d2f173fc083d37aa7761d550848ca5..692d65b44ce1645954a1188489ebe77578aa11ac 100644 (file)
@@ -321,6 +321,8 @@ func SettingsPost(ctx *context.Context) {
                                        AllowRebase:               form.PullsAllowRebase,
                                        AllowRebaseMerge:          form.PullsAllowRebaseMerge,
                                        AllowSquash:               form.PullsAllowSquash,
+                                       AllowManualMerge:          form.PullsAllowManualMerge,
+                                       AutodetectManualMerge:     form.EnableAutodetectManualMerge,
                                },
                        })
                } else if !models.UnitTypePullRequests.UnitGlobalDisabled() {
index 5acee8174bf38f0cb9021e7a45a423eda2372384..3ec76de5e8748a1c39ae26ead71ff68f6a23fe17 100644 (file)
@@ -116,7 +116,7 @@ func getMergeCommit(pr *models.PullRequest) (*git.Commit, error) {
        if err != nil {
                return nil, fmt.Errorf("git rev-list --ancestry-path --merges --reverse: %v", err)
        } else if len(mergeCommit) < 40 {
-               // PR was fast-forwarded, so just use last commit of PR
+               // PR was maybe fast-forwarded, so just use last commit of PR
                mergeCommit = commitID[:40]
        }
 
@@ -137,6 +137,21 @@ func getMergeCommit(pr *models.PullRequest) (*git.Commit, error) {
 // manuallyMerged checks if a pull request got manually merged
 // When a pull request got manually merged mark the pull request as merged
 func manuallyMerged(pr *models.PullRequest) bool {
+       if err := pr.LoadBaseRepo(); err != nil {
+               log.Error("PullRequest[%d].LoadBaseRepo: %v", pr.ID, err)
+               return false
+       }
+
+       if unit, err := pr.BaseRepo.GetUnit(models.UnitTypePullRequests); err == nil {
+               config := unit.PullRequestsConfig()
+               if !config.AutodetectManualMerge {
+                       return false
+               }
+       } else {
+               log.Error("PullRequest[%d].BaseRepo.GetUnit(models.UnitTypePullRequests): %v", pr.ID, err)
+               return false
+       }
+
        commit, err := getMergeCommit(pr)
        if err != nil {
                log.Error("PullRequest[%d].getMergeCommit: %v", pr.ID, err)
index 9d6eadc524dd98a2d887c179461a901d92ca4e60..bbe631f68c9979632523ef9dd577f0c27bf0ba51 100644 (file)
@@ -615,3 +615,54 @@ func CheckPRReadyToMerge(pr *models.PullRequest, skipProtectedFilesCheck bool) (
 
        return nil
 }
+
+// MergedManually mark pr as merged manually
+func MergedManually(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repository, commitID string) (err error) {
+       prUnit, err := pr.BaseRepo.GetUnit(models.UnitTypePullRequests)
+       if err != nil {
+               return
+       }
+       prConfig := prUnit.PullRequestsConfig()
+
+       // Check if merge style is correct and allowed
+       if !prConfig.IsMergeStyleAllowed(models.MergeStyleManuallyMerged) {
+               return models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: models.MergeStyleManuallyMerged}
+       }
+
+       if len(commitID) < 40 {
+               return fmt.Errorf("Wrong commit ID")
+       }
+
+       commit, err := baseGitRepo.GetCommit(commitID)
+       if err != nil {
+               if git.IsErrNotExist(err) {
+                       return fmt.Errorf("Wrong commit ID")
+               }
+               return
+       }
+
+       ok, err := baseGitRepo.IsCommitInBranch(commitID, pr.BaseBranch)
+       if err != nil {
+               return
+       }
+       if !ok {
+               return fmt.Errorf("Wrong commit ID")
+       }
+
+       pr.MergedCommitID = commitID
+       pr.MergedUnix = timeutil.TimeStamp(commit.Author.When.Unix())
+       pr.Status = models.PullRequestStatusManuallyMerged
+       pr.Merger = doer
+       pr.MergerID = doer.ID
+
+       merged := false
+       if merged, err = pr.SetMerged(); err != nil {
+               return
+       } else if !merged {
+               return fmt.Errorf("SetMerged failed")
+       }
+
+       notification.NotifyMergePullRequest(pr, doer)
+       log.Info("manuallyMerged[%d]: Marked as manually merged into %s/%s by commit id: %s", pr.ID, pr.BaseRepo.Name, pr.BaseBranch, commit.ID.String())
+       return nil
+}
index 2692d848c433b6f1363d95209f44d345cecb217c..72b459bf2cb3d0fcc20d78289e52659cdf1b1a2c 100644 (file)
@@ -80,7 +80,7 @@ func TestPatch(pr *models.PullRequest) error {
        pr.MergeBase = strings.TrimSpace(pr.MergeBase)
 
        // 2. Check for conflicts
-       if conflicts, err := checkConflicts(pr, gitRepo, tmpBasePath); err != nil || conflicts {
+       if conflicts, err := checkConflicts(pr, gitRepo, tmpBasePath); err != nil || conflicts || pr.Status == models.PullRequestStatusEmpty {
                return err
        }
 
@@ -125,8 +125,9 @@ func checkConflicts(pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath
        // 1a. if the size of that patch is 0 - there can be no conflicts!
        if stat.Size() == 0 {
                log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID)
-               pr.Status = models.PullRequestStatusMergeable
+               pr.Status = models.PullRequestStatusEmpty
                pr.ConflictedFiles = []string{}
+               pr.ChangedProtectedFiles = []string{}
                return false, nil
        }
 
index 245bdaa54292d0315b43f6510c497ae5989f9f1f..071a79045792fb3a18cfe92abb56f2173ca68b09 100644 (file)
        {{end}}
 
        {{if .IsNothingToCompare}}
-       <div class="ui segment">{{.i18n.Tr "repo.pulls.nothing_to_compare"}}</div>
+               {{if and $.IsSigned $.AllowEmptyPr (not .Repository.IsArchived) }}
+                       <div class="ui segment">{{.i18n.Tr "repo.pulls.nothing_to_compare_and_allow_empty_pr"}}</div>
+                       <div class="ui info message show-form-container">
+                               <button class="ui button green show-form">{{.i18n.Tr "repo.pulls.new"}}</button>
+                       </div>
+                       <div class="pullrequest-form" style="display: none">
+                               {{template "repo/issue/new_form" .}}
+                       </div>
+               {{else}}
+                       <div class="ui segment">{{.i18n.Tr "repo.pulls.nothing_to_compare"}}</div>
+               {{end}}
        {{else if and .PageIsComparePull (gt .CommitCount 0)}}
                {{if .HasPullRequest}}
                <div class="ui segment">
index cfacde96486fec74f9fe79ed87c9812bc244dc7e..b8d78f56972469861cf5870b4b790135b43cac50 100644 (file)
                        <span class="text grey">
                                <a class="author" href="{{.Poster.HomeLink}}">{{.Poster.GetDisplayName}}</a>
                                {{$link := printf "%s/commit/%s" $.Repository.HTMLURL $.Issue.PullRequest.MergedCommitID}}
-                               {{$.i18n.Tr "repo.issues.pull_merged_at" $link (ShortSha $.Issue.PullRequest.MergedCommitID) ($.BaseTarget|Escape) $createdStr | Str2html}}
+                               {{if eq $.Issue.PullRequest.Status 3}}
+                                       {{$.i18n.Tr "repo.issues.manually_pull_merged_at" $link (ShortSha $.Issue.PullRequest.MergedCommitID) $.BaseTarget $createdStr | Str2html}}
+                               {{else}}
+                                       {{$.i18n.Tr "repo.issues.pull_merged_at" $link (ShortSha $.Issue.PullRequest.MergedCommitID) $.BaseTarget $createdStr | Str2html}}
+                               {{end}}
                        </span>
                </div>
        {{else if eq .Type 3 5 6}}
index 9e883c0a9352d477c0d039e68f43440211a36f88..2175fad0673fd71b0f390243ffe0b2caf0bc48a8 100644 (file)
        {{- else if and .EnableStatusCheck (or (not $.LatestCommitStatus) .RequiredStatusCheckState.IsPending .RequiredStatusCheckState.IsWarning)}}yellow
        {{- else if and .AllowMerge .RequireSigned (not .WillSign)}}red
        {{- else if .Issue.PullRequest.IsChecking}}yellow
+       {{- else if .Issue.PullRequest.IsEmpty}}grey
        {{- else if .Issue.PullRequest.CanAutoMerge}}green
        {{- else}}red{{end}}">{{svg "octicon-git-merge" 32}}</a>
        <div class="content">
                                <div class="item text">
                                        {{if .Issue.PullRequest.MergedCommitID}}
                                                {{$link := printf "%s/commit/%s" $.Repository.HTMLURL .Issue.PullRequest.MergedCommitID}}
-                                               {{$.i18n.Tr "repo.pulls.merged_as" $link (ShortSha .Issue.PullRequest.MergedCommitID) | Safe}}
+                                               {{if eq $.Issue.PullRequest.Status 3}}
+                                                       {{$.i18n.Tr "repo.pulls.manually_merged_as" $link (ShortSha .Issue.PullRequest.MergedCommitID) | Safe}}
+                                               {{else}}
+                                                       {{$.i18n.Tr "repo.pulls.merged_as" $link (ShortSha .Issue.PullRequest.MergedCommitID) | Safe}}
+                                               {{end}}
                                        {{else}}
                                                {{$.i18n.Tr "repo.pulls.has_merged"}}
                                        {{end}}
                                        <i class="icon icon-octicon">{{svg "octicon-sync"}}</i>
                                        {{$.i18n.Tr "repo.pulls.is_checking"}}
                                </div>
+                       {{else if .Issue.PullRequest.IsEmpty}}
+                               <div class="item text grey">
+                                       <i class="icon icon-octicon">{{svg "octicon-alert" 16}}</i>
+                                       {{$.i18n.Tr "repo.pulls.is_empty"}}
+                               </div>
                        {{else if .Issue.PullRequest.CanAutoMerge}}
                                {{if .IsBlockedByApprovals}}
                                        <div class="item">
                                                                </form>
                                                        </div>
                                                        {{end}}
+                                                       {{if and $prUnit.PullRequestsConfig.AllowManualMerge $.IsRepoAdmin}}
+                                                               <div class="ui form manually-merged-fields" style="display: none">
+                                                                       <form action="{{.Link}}/merge" method="post">
+                                                                               {{.CsrfTokenHtml}}
+                                                                               <div class="field">
+                                                                                       <input type="text" name="merge_commit_id"  placeholder="{{$.i18n.Tr "repo.pulls.merge_commit_id"}}">
+                                                                               </div>
+                                                                               <button class="ui red button" type="submit" name="do" value="manually-merged">
+                                                                                       {{$.i18n.Tr "repo.pulls.merge_manually"}}
+                                                                               </button>
+                                                                               <button class="ui button merge-cancel">
+                                                                                       {{$.i18n.Tr "cancel"}}
+                                                                               </button>
+                                                                       </form>
+                                                               </div>
+                                                       {{end}}
                                                        <div class="dib">
                                                                <div class="ui {{if $notAllOverridableChecksOk}}red{{else}}green{{end}} buttons merge-button">
                                                                        <button class="ui button" data-do="{{.MergeStyle}}">
                                                                                {{if eq .MergeStyle "squash"}}
                                                                                        {{$.i18n.Tr "repo.pulls.squash_merge_pull_request"}}
                                                                                {{end}}
+                                                                               {{if eq .MergeStyle "manually-merged"}}
+                                                                                       {{$.i18n.Tr "repo.pulls.merge_manually"}}
+                                                                               {{end}}
                                                                                </span>
                                                                        </button>
                                                                        {{if gt $prUnit.PullRequestsConfig.AllowedMergeStyleCount 1}}
                                                                                                {{if $prUnit.PullRequestsConfig.AllowSquash}}
                                                                                                <div class="item{{if eq .MergeStyle "squash"}} active selected{{end}}" data-do="squash">{{$.i18n.Tr "repo.pulls.squash_merge_pull_request"}}</div>
                                                                                                {{end}}
+                                                                                               {{if and $prUnit.PullRequestsConfig.AllowManualMerge $.IsRepoAdmin}}
+                                                                                               <div class="item{{if eq .MergeStyle "manually-merged"}} active selected{{end}}" data-do="manually-merged">{{$.i18n.Tr "repo.pulls.merge_manually"}}</div>
+                                                                                               {{end}}
                                                                                        </div>
                                                                                </div>
                                                                        {{end}}
                                        {{end}}
                                </div>
                        {{end}}
+
+                       {{if $.StillCanManualMerge}}
+                               <div class="ui divider"></div>
+                               <div class="ui form manually-merged-fields" style="display: none">
+                                       <form action="{{.Link}}/merge" method="post">
+                                               {{.CsrfTokenHtml}}
+                                               <div class="field">
+                                                       <input type="text" name="merge_commit_id"  placeholder="{{$.i18n.Tr "repo.pulls.merge_commit_id"}}">
+                                               </div>
+                                               <button class="ui red button" type="submit" name="do" value="manually-merged">
+                                                       {{$.i18n.Tr "repo.pulls.merge_manually"}}
+                                               </button>
+                                               <button class="ui button merge-cancel">
+                                                       {{$.i18n.Tr "cancel"}}
+                                               </button>
+                                       </form>
+                               </div>
+
+                               <div class="ui red buttons merge-button">
+                                       <button class="ui button" data-do="manually-merged">
+                                               {{$.i18n.Tr "repo.pulls.merge_manually"}}
+                                       </button>
+                               </div>
+                       {{end}}
                </div>
        </div>
 </div>
index 2f2787d67ceb7beaf7c97081428b1f1696972a72..f6cbb9206ca117fe07ec0858945d5ca2a0a9c07b 100644 (file)
@@ -20,7 +20,7 @@
                {{end}}
        </div>
        {{if .HasMerged}}
-               <div class="ui purple large label">{{svg "octicon-git-merge"}} {{.i18n.Tr "repo.pulls.merged"}}</div>
+               <div class="ui purple large label">{{svg "octicon-git-merge" 16}} {{if eq .Issue.PullRequest.Status 3}}{{.i18n.Tr "repo.pulls.manually_merged"}}{{else}}{{.i18n.Tr "repo.pulls.merged"}}{{end}}</div>
        {{else if .Issue.IsClosed}}
                <div class="ui red large label">{{if .Issue.IsPull}}{{svg "octicon-git-pull-request"}}{{else}}{{svg "octicon-issue-closed"}}{{end}} {{.i18n.Tr "repo.issues.closed_title"}}</div>
        {{else if .Issue.IsPull}}
index b69f90f9c5849f59fa81792f8d45a050d52a56b0..9d87101671ffbf88bb03aa78cef5f6d7406e8c9d 100644 (file)
                                                                <label>{{.i18n.Tr "repo.settings.pulls.allow_squash_commits"}}</label>
                                                        </div>
                                                </div>
+                                               <div class="field">
+                                                       <div class="ui checkbox">
+                                                               <input name="pulls_allow_manual_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowManualMerge)}}checked{{end}}>
+                                                               <label>{{.i18n.Tr "repo.settings.pulls.allow_manual_merge"}}</label>
+                                                       </div>
+                                               </div>
+                                               <div class="field">
+                                                       <div class="ui checkbox">
+                                                               <input name="enable_autodetect_manual_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AutodetectManualMerge)}}checked{{end}}>
+                                                               <label>{{.i18n.Tr "repo.settings.pulls.enable_autodetect_manual_merge"}}</label>
+                                                       </div>
+                                               </div>
                                        </div>
                                {{end}}
 
index 1b1d9e5c970fd58afa4ebf3b898c7dd4185d793b..930af907ea8bdf38e27559b1ae87d10a0c0afaef 100644 (file)
       "description": "EditRepoOption options when editing a repository's properties",
       "type": "object",
       "properties": {
+        "allow_manual_merge": {
+          "description": "either `true` to allow mark pr as merged manually, or `false` to prevent it. `has_pull_requests` must be `true`.",
+          "type": "boolean",
+          "x-go-name": "AllowManualMerge"
+        },
         "allow_merge_commits": {
           "description": "either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits. `has_pull_requests` must be `true`.",
           "type": "boolean",
           "type": "boolean",
           "x-go-name": "Archived"
         },
+        "autodetect_manual_merge": {
+          "description": "either `true` to enable AutodetectManualMerge, or `false` to prevent it. `has_pull_requests` must be `true`, Note: In some special cases, misjudgments can occur.",
+          "type": "boolean",
+          "x-go-name": "AutodetectManualMerge"
+        },
         "default_branch": {
           "description": "sets the default branch for this repository.",
           "type": "string",
             "merge",
             "rebase",
             "rebase-merge",
-            "squash"
+            "squash",
+            "manually-merged"
           ]
         },
+        "MergeCommitID": {
+          "type": "string"
+        },
         "MergeMessageField": {
           "type": "string"
         },