From 8420c1bf4c46a59973d30af5114216918d0f60cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ing=2E=20Jaroslav=20=C5=A0afka?= <devel@safka.org>
Date: Wed, 13 Jul 2022 10:22:51 +0200
Subject: [PATCH] Fix checks in PR for empty commits #19603 (#20290)

* Fixes issue #19603 (Not able to merge commit in PR when branches content is same, but different commit id)
* fill HeadCommitID in PullRequest
* compare real commits ID as check for merging
* based on @zeripath patch in #19738
---
 integrations/pull_status_test.go              | 30 +++++++++++++++++--
 models/issues/pull.go                         |  6 ++++
 options/locale/locale_en-US.ini               |  3 +-
 services/pull/check.go                        |  2 +-
 services/pull/patch.go                        |  8 +++++
 templates/repo/issue/view_content/pull.tmpl   | 16 +++++++---
 .../js/components/PullRequestMergeForm.vue    |  2 +-
 7 files changed, 58 insertions(+), 9 deletions(-)

diff --git a/integrations/pull_status_test.go b/integrations/pull_status_test.go
index a5247f56ec..33a27cd812 100644
--- a/integrations/pull_status_test.go
+++ b/integrations/pull_status_test.go
@@ -105,7 +105,11 @@ func doAPICreateCommitStatus(ctx APITestContext, commitID string, status api.Com
 	}
 }
 
-func TestPullCreate_EmptyChangesWithCommits(t *testing.T) {
+func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) {
+	// Merge must continue if commits SHA are different, even if content is same
+	// Reason: gitflow and merging master back into develop, where is high possiblity, there are no changes
+	// but just commit saying "Merge branch". And this meta commit can be also tagged,
+	// so we need to have this meta commit also in develop branch.
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 		session := loginUser(t, "user1")
 		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
@@ -126,6 +130,28 @@ 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 branch is equal with the target branch.")
+		assert.Contains(t, text, "This pull request can be merged automatically.")
+	})
+}
+
+func TestPullCreate_EmptyChangesWithSameCommits(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, u *url.URL) {
+		session := loginUser(t, "user1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testCreateBranch(t, session, "user1", "repo1", "branch/master", "status1", http.StatusSeeOther)
+		url := path.Join("user1", "repo1", "compare", "master...status1")
+		req := NewRequestWithValues(t, "POST", url,
+			map[string]string{
+				"_csrf": GetCSRF(t, session, url),
+				"title": "pull request from status1",
+			},
+		)
+		session.MakeRequest(t, req, http.StatusSeeOther)
+		req = NewRequest(t, "GET", "/user1/repo1/pulls/1")
+		resp := session.MakeRequest(t, req, http.StatusOK)
+		doc := NewHTMLParser(t, resp.Body)
+
+		text := strings.TrimSpace(doc.doc.Find(".merge-section").Text())
+		assert.Contains(t, text, "This branch is already included in the target branch. There is nothing to merge.")
 	})
 }
diff --git a/models/issues/pull.go b/models/issues/pull.go
index 52b9596889..f96b03445e 100644
--- a/models/issues/pull.go
+++ b/models/issues/pull.go
@@ -122,6 +122,7 @@ const (
 	PullRequestStatusManuallyMerged
 	PullRequestStatusError
 	PullRequestStatusEmpty
+	PullRequestStatusAncestor
 )
 
 // PullRequestFlow the flow of pull request
@@ -423,6 +424,11 @@ func (pr *PullRequest) IsEmpty() bool {
 	return pr.Status == PullRequestStatusEmpty
 }
 
+// IsAncestor returns true if the Head Commit of this PR is an ancestor of the Base Commit
+func (pr *PullRequest) IsAncestor() bool {
+	return pr.Status == PullRequestStatusAncestor
+}
+
 // SetMerged sets a pull request to merged and closes the corresponding issue
 func (pr *PullRequest) SetMerged(ctx context.Context) (bool, error) {
 	if pr.HasMerged {
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 9b69d54593..9e8a030339 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1532,7 +1532,8 @@ pulls.remove_prefix = Remove <strong>%s</strong> prefix
 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.is_ancestor = "This branch is already included in the target branch. There is nothing to merge."
+pulls.is_empty = "The changes on this branch are already on the target branch. This will be an empty commit."
 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.
diff --git a/services/pull/check.go b/services/pull/check.go
index 6621a281fa..288f4dc0b7 100644
--- a/services/pull/check.go
+++ b/services/pull/check.go
@@ -89,7 +89,7 @@ func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *acce
 			return ErrIsWorkInProgress
 		}
 
-		if !pr.CanAutoMerge() {
+		if !pr.CanAutoMerge() && !pr.IsEmpty() {
 			return ErrNotMergableState
 		}
 
diff --git a/services/pull/patch.go b/services/pull/patch.go
index c7a69501c3..bb09acc89f 100644
--- a/services/pull/patch.go
+++ b/services/pull/patch.go
@@ -87,6 +87,14 @@ func TestPatch(pr *issues_model.PullRequest) error {
 		}
 	}
 	pr.MergeBase = strings.TrimSpace(pr.MergeBase)
+	if pr.HeadCommitID, err = gitRepo.GetRefCommitID(git.BranchPrefix + "tracking"); err != nil {
+		return fmt.Errorf("GetBranchCommitID: can't find commit ID for head: %w", err)
+	}
+
+	if pr.HeadCommitID == pr.MergeBase {
+		pr.Status = issues_model.PullRequestStatusAncestor
+		return nil
+	}
 
 	// 2. Check for conflicts
 	if conflicts, err := checkConflicts(ctx, pr, gitRepo, tmpBasePath); err != nil || conflicts || pr.Status == issues_model.PullRequestStatusEmpty {
diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl
index 179f857ba6..60fe667a54 100644
--- a/templates/repo/issue/view_content/pull.tmpl
+++ b/templates/repo/issue/view_content/pull.tmpl
@@ -195,12 +195,12 @@
 					<i class="icon icon-octicon">{{svg "octicon-sync"}}</i>
 					{{$.locale.Tr "repo.pulls.is_checking"}}
 				</div>
-			{{else if .Issue.PullRequest.IsEmpty}}
+			{{else if .Issue.PullRequest.IsAncestor}}
 				<div class="item">
 					<i class="icon icon-octicon">{{svg "octicon-alert" 16}}</i>
-					{{$.locale.Tr "repo.pulls.is_empty"}}
+					{{$.locale.Tr "repo.pulls.is_ancestor"}}
 				</div>
-			{{else if .Issue.PullRequest.CanAutoMerge}}
+			{{else if or .Issue.PullRequest.CanAutoMerge .Issue.PullRequest.IsEmpty}}
 				{{if .IsBlockedByApprovals}}
 					<div class="item">
 						<i class="icon icon-octicon">{{svg "octicon-x"}}</i>
@@ -282,7 +282,6 @@
 						</div>
 					{{end}}
 				{{end}}
-
 				{{if and (gt .Issue.PullRequest.CommitsBehind 0) (not  .Issue.IsClosed) (not .Issue.PullRequest.IsChecking) (not .IsPullFilesConflicted) (not .IsPullRequestBroken) (not $canAutoMerge)}}
 					<div class="ui divider"></div>
 					<div class="item item-section">
@@ -321,6 +320,14 @@
 						</div>
 					</div>
 				{{end}}
+				{{if .Issue.PullRequest.IsEmpty}}
+					<div class="ui divider"></div>
+
+					<div class="item">
+						<i class="icon icon-octicon">{{svg "octicon-alert" 16}}</i>
+						{{$.locale.Tr "repo.pulls.is_empty"}}
+					</div>
+				{{end}}
 
 				{{if .AllowMerge}} {{/* user is allowed to merge */}}
 					{{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}}
@@ -348,6 +355,7 @@
 
 									'canMergeNow': {{$canMergeNow}},
 									'allOverridableChecksOk': {{not $notAllOverridableChecksOk}},
+									'emptyCommit': {{.Issue.PullRequest.IsEmpty}},
 									'pullHeadCommitID': {{.PullHeadCommitID}},
 									'isPullBranchDeletable': {{.IsPullBranchDeletable}},
 									'defaultDeleteBranchAfterMerge': {{$prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge}},
diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue
index 75fbceb800..08b1f9cb86 100644
--- a/web_src/js/components/PullRequestMergeForm.vue
+++ b/web_src/js/components/PullRequestMergeForm.vue
@@ -48,7 +48,7 @@
 
     <div v-if="!showActionForm" class="df">
       <!-- the merge button -->
-      <div class="ui buttons merge-button" :class="mergeButtonStyleClass" @click="toggleActionForm(true)" >
+      <div class="ui buttons merge-button" :class="[mergeForm.emptyCommit ? 'grey' : mergeForm.allOverridableChecksOk ? 'green' : 'red']" @click="toggleActionForm(true)" >
         <button class="ui button">
           <svg-icon name="octicon-git-merge"/>
           <span class="button-text">