Skip to content

Commit a5279b7

Browse files
a1012112796zeripathsilverwindtechknowlogick
authored
Make manual merge autodetection optional and add manual merge as merge method (#12543)
* 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 <[email protected]> * make swager * api support * ping ci * fix TestPullCreate_EmptyChangesWithCommits * Apply suggestions from code review Co-authored-by: zeripath <[email protected]> * Apply review suggestions and add test * Apply suggestions from code review Co-authored-by: zeripath <[email protected]> * fix build * test error message * make fmt * Fix indentation issues identified by @silverwind Co-authored-by: silverwind <[email protected]> * Fix tests and make manually merged disabled error on API the same Signed-off-by: Andrew Thornton <[email protected]> * a small nit * fix wrong commit id error * fix bug * simple test * fix test Co-authored-by: zeripath <[email protected]> Co-authored-by: silverwind <[email protected]> Co-authored-by: techknowlogick <[email protected]>
1 parent 523efa4 commit a5279b7

25 files changed

+379
-23
lines changed

integrations/api_helper_for_declarative_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"io/ioutil"
1111
"net/http"
12+
"net/url"
1213
"testing"
1314
"time"
1415

@@ -71,6 +72,23 @@ func doAPICreateRepository(ctx APITestContext, empty bool, callback ...func(*tes
7172
}
7273
}
7374

75+
func doAPIEditRepository(ctx APITestContext, editRepoOption *api.EditRepoOption, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
76+
return func(t *testing.T) {
77+
req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), ctx.Token), editRepoOption)
78+
if ctx.ExpectedCode != 0 {
79+
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
80+
return
81+
}
82+
resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
83+
84+
var repository api.Repository
85+
DecodeJSON(t, resp, &repository)
86+
if len(callback) > 0 {
87+
callback[0](t, repository)
88+
}
89+
}
90+
}
91+
7492
func doAPIAddCollaborator(ctx APITestContext, username string, mode models.AccessMode) func(*testing.T) {
7593
return func(t *testing.T) {
7694
permission := "read"
@@ -256,6 +274,23 @@ func doAPIMergePullRequest(ctx APITestContext, owner, repo string, index int64)
256274
}
257275
}
258276

277+
func doAPIManuallyMergePullRequest(ctx APITestContext, owner, repo, commitID string, index int64) func(*testing.T) {
278+
return func(t *testing.T) {
279+
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge?token=%s",
280+
owner, repo, index, ctx.Token)
281+
req := NewRequestWithJSON(t, http.MethodPost, urlStr, &auth.MergePullRequestForm{
282+
Do: string(models.MergeStyleManuallyMerged),
283+
MergeCommitID: commitID,
284+
})
285+
286+
if ctx.ExpectedCode != 0 {
287+
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
288+
return
289+
}
290+
ctx.Session.MakeRequest(t, req, 200)
291+
}
292+
}
293+
259294
func doAPIGetBranch(ctx APITestContext, branch string, callback ...func(*testing.T, api.Branch)) func(*testing.T) {
260295
return func(t *testing.T) {
261296
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/branches/%s?token=%s", ctx.Username, ctx.Reponame, branch, ctx.Token)

integrations/git_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ func testGit(t *testing.T, u *url.URL) {
6969
mediaTest(t, &httpContext, little, big, littleLFS, bigLFS)
7070

7171
t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath))
72+
t.Run("CreatePRAndSetManuallyMerged", doCreatePRAndSetManuallyMerged(httpContext, httpContext, dstPath, "master", "test-manually-merge"))
7273
t.Run("MergeFork", func(t *testing.T) {
7374
defer PrintCurrentTest(t)()
7475
t.Run("CreatePRAndMerge", doMergeFork(httpContext, forkedUserCtx, "master", httpContext.Username+":master"))
@@ -468,6 +469,35 @@ func doMergeFork(ctx, baseCtx APITestContext, baseBranch, headBranch string) fun
468469
}
469470
}
470471

472+
func doCreatePRAndSetManuallyMerged(ctx, baseCtx APITestContext, dstPath, baseBranch, headBranch string) func(t *testing.T) {
473+
return func(t *testing.T) {
474+
defer PrintCurrentTest(t)()
475+
var (
476+
pr api.PullRequest
477+
err error
478+
lastCommitID string
479+
)
480+
481+
trueBool := true
482+
falseBool := false
483+
484+
t.Run("AllowSetManuallyMergedAndSwitchOffAutodetectManualMerge", doAPIEditRepository(baseCtx, &api.EditRepoOption{
485+
HasPullRequests: &trueBool,
486+
AllowManualMerge: &trueBool,
487+
AutodetectManualMerge: &falseBool,
488+
}))
489+
490+
t.Run("CreateHeadBranch", doGitCreateBranch(dstPath, headBranch))
491+
t.Run("PushToHeadBranch", doGitPushTestRepository(dstPath, "origin", headBranch))
492+
t.Run("CreateEmptyPullRequest", func(t *testing.T) {
493+
pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, baseBranch, headBranch)(t)
494+
assert.NoError(t, err)
495+
})
496+
lastCommitID = pr.Base.Sha
497+
t.Run("ManuallyMergePR", doAPIManuallyMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, lastCommitID, pr.Index))
498+
}
499+
}
500+
471501
func doEnsureCanSeePull(ctx APITestContext, pr api.PullRequest) func(t *testing.T) {
472502
return func(t *testing.T) {
473503
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index))

integrations/pull_status_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,6 @@ func TestPullCreate_EmptyChangesWithCommits(t *testing.T) {
115115
doc := NewHTMLParser(t, resp.Body)
116116

117117
text := strings.TrimSpace(doc.doc.Find(".merge-section").Text())
118-
assert.Contains(t, text, "This pull request can be merged automatically.")
118+
assert.Contains(t, text, "This branch is equal with the target branch.")
119119
})
120120
}

models/pull.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const (
3535
PullRequestStatusMergeable
3636
PullRequestStatusManuallyMerged
3737
PullRequestStatusError
38+
PullRequestStatusEmpty
3839
)
3940

4041
// PullRequest represents relation between pull request and repositories.
@@ -332,6 +333,11 @@ func (pr *PullRequest) CanAutoMerge() bool {
332333
return pr.Status == PullRequestStatusMergeable
333334
}
334335

336+
// IsEmpty returns true if this pull request is empty.
337+
func (pr *PullRequest) IsEmpty() bool {
338+
return pr.Status == PullRequestStatusEmpty
339+
}
340+
335341
// MergeStyle represents the approach to merge commits into base branch.
336342
type MergeStyle string
337343

@@ -344,6 +350,8 @@ const (
344350
MergeStyleRebaseMerge MergeStyle = "rebase-merge"
345351
// MergeStyleSquash squash commits into single commit before merging
346352
MergeStyleSquash MergeStyle = "squash"
353+
// MergeStyleManuallyMerged pr has been merged manually, just mark it as merged directly
354+
MergeStyleManuallyMerged MergeStyle = "manually-merged"
347355
)
348356

349357
// SetMerged sets a pull request to merged and closes the corresponding issue

models/repo_unit.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ type PullRequestsConfig struct {
101101
AllowRebase bool
102102
AllowRebaseMerge bool
103103
AllowSquash bool
104+
AllowManualMerge bool
105+
AutodetectManualMerge bool
104106
}
105107

106108
// FromDB fills up a PullRequestsConfig from serialized format.
@@ -120,7 +122,8 @@ func (cfg *PullRequestsConfig) IsMergeStyleAllowed(mergeStyle MergeStyle) bool {
120122
return mergeStyle == MergeStyleMerge && cfg.AllowMerge ||
121123
mergeStyle == MergeStyleRebase && cfg.AllowRebase ||
122124
mergeStyle == MergeStyleRebaseMerge && cfg.AllowRebaseMerge ||
123-
mergeStyle == MergeStyleSquash && cfg.AllowSquash
125+
mergeStyle == MergeStyleSquash && cfg.AllowSquash ||
126+
mergeStyle == MergeStyleManuallyMerged && cfg.AllowManualMerge
124127
}
125128

126129
// AllowedMergeStyleCount returns the total count of allowed merge styles for the PullRequestsConfig

modules/forms/repo_form.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ type RepoSettingForm struct {
156156
PullsAllowRebase bool
157157
PullsAllowRebaseMerge bool
158158
PullsAllowSquash bool
159+
PullsAllowManualMerge bool
160+
EnableAutodetectManualMerge bool
159161
EnableTimetracker bool
160162
AllowOnlyContributorsToTrackTime bool
161163
EnableIssueDependencies bool
@@ -556,11 +558,12 @@ func (f *InitializeLabelsForm) Validate(req *http.Request, errs binding.Errors)
556558
// swagger:model MergePullRequestOption
557559
type MergePullRequestForm struct {
558560
// required: true
559-
// enum: merge,rebase,rebase-merge,squash
560-
Do string `binding:"Required;In(merge,rebase,rebase-merge,squash)"`
561+
// enum: merge,rebase,rebase-merge,squash,manually-merged
562+
Do string `binding:"Required;In(merge,rebase,rebase-merge,squash,manually-merged)"`
561563
MergeTitleField string
562564
MergeMessageField string
563-
ForceMerge *bool `json:"force_merge,omitempty"`
565+
MergeCommitID string // only used for manually-merged
566+
ForceMerge *bool `json:"force_merge,omitempty"`
564567
}
565568

566569
// Validate validates the fields

modules/git/repo_commit.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,3 +456,12 @@ func (repo *Repository) GetCommitsFromIDs(commitIDs []string) (commits *list.Lis
456456

457457
return commits
458458
}
459+
460+
// IsCommitInBranch check if the commit is on the branch
461+
func (repo *Repository) IsCommitInBranch(commitID, branch string) (r bool, err error) {
462+
stdout, err := NewCommand("branch", "--contains", commitID, branch).RunInDir(repo.Path)
463+
if err != nil {
464+
return false, err
465+
}
466+
return len(stdout) > 0, err
467+
}

modules/git/repo_commit_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,18 @@ func TestGetCommitWithBadCommitID(t *testing.T) {
6363
assert.Error(t, err)
6464
assert.True(t, IsErrNotExist(err))
6565
}
66+
67+
func TestIsCommitInBranch(t *testing.T) {
68+
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
69+
bareRepo1, err := OpenRepository(bareRepo1Path)
70+
assert.NoError(t, err)
71+
defer bareRepo1.Close()
72+
73+
result, err := bareRepo1.IsCommitInBranch("2839944139e0de9737a044f78b0e4b40d989a9e3", "branch1")
74+
assert.NoError(t, err)
75+
assert.Equal(t, true, result)
76+
77+
result, err = bareRepo1.IsCommitInBranch("2839944139e0de9737a044f78b0e4b40d989a9e3", "branch2")
78+
assert.NoError(t, err)
79+
assert.Equal(t, false, result)
80+
}

modules/structs/repo.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ type EditRepoOption struct {
167167
AllowRebaseMerge *bool `json:"allow_rebase_explicit,omitempty"`
168168
// either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging. `has_pull_requests` must be `true`.
169169
AllowSquash *bool `json:"allow_squash_merge,omitempty"`
170+
// either `true` to allow mark pr as merged manually, or `false` to prevent it. `has_pull_requests` must be `true`.
171+
AllowManualMerge *bool `json:"allow_manual_merge,omitempty"`
172+
// either `true` to enable AutodetectManualMerge, or `false` to prevent it. `has_pull_requests` must be `true`, Note: In some special cases, misjudgments can occur.
173+
AutodetectManualMerge *bool `json:"autodetect_manual_merge,omitempty"`
170174
// set to `true` to archive this repository.
171175
Archived *bool `json:"archived,omitempty"`
172176
// set to a string like `8h30m0s` to set the mirror interval time

options/locale/locale_en-US.ini

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,6 +1105,7 @@ issues.context.delete = Delete
11051105
issues.no_content = There is no content yet.
11061106
issues.close_issue = Close
11071107
issues.pull_merged_at = `merged commit <a href="%[1]s">%[2]s</a> into <b>%[3]s</b> %[4]s`
1108+
issues.manually_pull_merged_at = `merged commit <a href="%[1]s">%[2]s</a> into <b>%[3]s</b> %[4]s manually`
11081109
issues.close_comment_issue = Comment and Close
11091110
issues.reopen_issue = Reopen
11101111
issues.reopen_comment_issue = Comment and Reopen
@@ -1273,6 +1274,7 @@ pulls.compare_compare = pull from
12731274
pulls.filter_branch = Filter branch
12741275
pulls.no_results = No results found.
12751276
pulls.nothing_to_compare = These branches are equal. There is no need to create a pull request.
1277+
pulls.nothing_to_compare_and_allow_empty_pr = These branches are equal. This PR will be empty.
12761278
pulls.has_pull_request = `A pull request between these branches already exists: <a href="%[1]s/pulls/%[3]d">%[2]s#%[3]d</a>`
12771279
pulls.create = Create Pull Request
12781280
pulls.title_desc = wants to merge %[1]d commits from <code>%[2]s</code> into <code id="branch_target">%[3]s</code>
@@ -1285,13 +1287,16 @@ pulls.reopen_to_merge = Please reopen this pull request to perform a merge.
12851287
pulls.cant_reopen_deleted_branch = This pull request cannot be reopened because the branch was deleted.
12861288
pulls.merged = Merged
12871289
pulls.merged_as = The pull request has been merged as <a rel="nofollow" class="ui sha" href="%[1]s"><code>%[2]s</code></a>.
1290+
pulls.manually_merged = Manually merged
1291+
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>.
12881292
pulls.is_closed = The pull request has been closed.
12891293
pulls.has_merged = The pull request has been merged.
12901294
pulls.title_wip_desc = `<a href="#">Start the title with <strong>%s</strong></a> to prevent the pull request from being merged accidentally.`
12911295
pulls.cannot_merge_work_in_progress = This pull request is marked as a work in progress. Remove the <strong>%s</strong> prefix from the title when it's ready
12921296
pulls.data_broken = This pull request is broken due to missing fork information.
12931297
pulls.files_conflicted = This pull request has changes conflicting with the target branch.
12941298
pulls.is_checking = "Merge conflict checking is in progress. Try again in few moments."
1299+
pulls.is_empty = "This branch is equal with the target branch."
12951300
pulls.required_status_check_failed = Some required checks were not successful.
12961301
pulls.required_status_check_missing = Some required checks are missing.
12971302
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"
13121317
pulls.reject_count_n = "%d change requests"
13131318
pulls.waiting_count_1 = "%d waiting review"
13141319
pulls.waiting_count_n = "%d waiting reviews"
1320+
pulls.wrong_commit_id = "commit id must be a commit id on the target branch"
13151321
13161322
pulls.no_merge_desc = This pull request cannot be merged because all repository merge options are disabled.
13171323
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
13221328
pulls.rebase_merge_pull_request = Rebase and Merge
13231329
pulls.rebase_merge_commit_pull_request = Rebase and Merge (--no-ff)
13241330
pulls.squash_merge_pull_request = Squash and Merge
1331+
pulls.merge_manually = Manually merged
1332+
pulls.merge_commit_id = The merge commit ID
13251333
pulls.require_signed_wont_sign = The branch requires signed commits but this merge will not be signed
13261334
pulls.invalid_merge_option = You cannot use this merge option for this pull request.
13271335
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
15451553
settings.pulls.allow_rebase_merge = Enable Rebasing to Merge Commits
15461554
settings.pulls.allow_rebase_merge_commit = Enable Rebasing with explicit merge commits (--no-ff)
15471555
settings.pulls.allow_squash_commits = Enable Squashing to Merge Commits
1556+
settings.pulls.allow_manual_merge = Enable Mark PR as manually merged
1557+
settings.pulls.enable_autodetect_manual_merge = Enable autodetect manual merge (Note: In some special cases, misjudgments can occur)
15481558
settings.projects_desc = Enable Repository Projects
15491559
settings.admin_settings = Administrator Settings
15501560
settings.admin_enable_health_check = Enable Repository Health Checks (git fsck)

routers/api/v1/repo/pull.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -769,13 +769,31 @@ func MergePullRequest(ctx *context.APIContext) {
769769
return
770770
}
771771

772-
if !pr.CanAutoMerge() {
773-
ctx.Error(http.StatusMethodNotAllowed, "PR not in mergeable state", "Please try again later")
772+
if pr.HasMerged {
773+
ctx.Error(http.StatusMethodNotAllowed, "PR already merged", "")
774774
return
775775
}
776776

777-
if pr.HasMerged {
778-
ctx.Error(http.StatusMethodNotAllowed, "PR already merged", "")
777+
// handle manually-merged mark
778+
if models.MergeStyle(form.Do) == models.MergeStyleManuallyMerged {
779+
if err = pull_service.MergedManually(pr, ctx.User, ctx.Repo.GitRepo, form.MergeCommitID); err != nil {
780+
if models.IsErrInvalidMergeStyle(err) {
781+
ctx.Error(http.StatusMethodNotAllowed, "Invalid merge style", fmt.Errorf("%s is not allowed an allowed merge style for this repository", models.MergeStyle(form.Do)))
782+
return
783+
}
784+
if strings.Contains(err.Error(), "Wrong commit ID") {
785+
ctx.JSON(http.StatusConflict, err)
786+
return
787+
}
788+
ctx.Error(http.StatusInternalServerError, "Manually-Merged", err)
789+
return
790+
}
791+
ctx.Status(http.StatusOK)
792+
return
793+
}
794+
795+
if !pr.CanAutoMerge() {
796+
ctx.Error(http.StatusMethodNotAllowed, "PR not in mergeable state", "Please try again later")
779797
return
780798
}
781799

routers/api/v1/repo/repo.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,8 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
725725
AllowRebase: true,
726726
AllowRebaseMerge: true,
727727
AllowSquash: true,
728+
AllowManualMerge: true,
729+
AutodetectManualMerge: false,
728730
}
729731
} else {
730732
config = unit.PullRequestsConfig()
@@ -745,6 +747,12 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
745747
if opts.AllowSquash != nil {
746748
config.AllowSquash = *opts.AllowSquash
747749
}
750+
if opts.AllowManualMerge != nil {
751+
config.AllowManualMerge = *opts.AllowManualMerge
752+
}
753+
if opts.AutodetectManualMerge != nil {
754+
config.AutodetectManualMerge = *opts.AutodetectManualMerge
755+
}
748756

749757
units = append(units, models.RepoUnit{
750758
RepoID: repo.ID,

routers/repo/compare.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,14 @@ func PrepareCompareDiff(
429429

430430
if headCommitID == compareInfo.MergeBase {
431431
ctx.Data["IsNothingToCompare"] = true
432+
if unit, err := repo.GetUnit(models.UnitTypePullRequests); err == nil {
433+
config := unit.PullRequestsConfig()
434+
if !config.AutodetectManualMerge {
435+
ctx.Data["AllowEmptyPr"] = !(baseBranch == headBranch && ctx.Repo.Repository.Name == headRepo.Name)
436+
} else {
437+
ctx.Data["AllowEmptyPr"] = false
438+
}
439+
}
432440
return true
433441
}
434442

routers/repo/issue.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1491,6 +1491,8 @@ func ViewIssue(ctx *context.Context) {
14911491
ctx.Data["MergeStyle"] = models.MergeStyleRebaseMerge
14921492
} else if prConfig.AllowSquash {
14931493
ctx.Data["MergeStyle"] = models.MergeStyleSquash
1494+
} else if prConfig.AllowManualMerge {
1495+
ctx.Data["MergeStyle"] = models.MergeStyleManuallyMerged
14941496
} else {
14951497
ctx.Data["MergeStyle"] = ""
14961498
}
@@ -1531,6 +1533,22 @@ func ViewIssue(ctx *context.Context) {
15311533
pull.HeadRepo != nil &&
15321534
git.IsBranchExist(pull.HeadRepo.RepoPath(), pull.HeadBranch) &&
15331535
(!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"])
1536+
1537+
stillCanManualMerge := func() bool {
1538+
if pull.HasMerged || issue.IsClosed || !ctx.IsSigned {
1539+
return false
1540+
}
1541+
if pull.CanAutoMerge() || pull.IsWorkInProgress() || pull.IsChecking() {
1542+
return false
1543+
}
1544+
if (ctx.User.IsAdmin || ctx.Repo.IsAdmin()) && prConfig.AllowManualMerge {
1545+
return true
1546+
}
1547+
1548+
return false
1549+
}
1550+
1551+
ctx.Data["StillCanManualMerge"] = stillCanManualMerge()
15341552
}
15351553

15361554
// Get Dependencies

0 commit comments

Comments
 (0)