code.gitea.io/gitea@v1.22.3/tests/integration/pull_merge_test.go (about)

     1  // Copyright 2017 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package integration
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"fmt"
    10  	"net/http"
    11  	"net/http/httptest"
    12  	"net/url"
    13  	"os"
    14  	"path"
    15  	"path/filepath"
    16  	"strconv"
    17  	"strings"
    18  	"testing"
    19  	"time"
    20  
    21  	"code.gitea.io/gitea/models"
    22  	auth_model "code.gitea.io/gitea/models/auth"
    23  	"code.gitea.io/gitea/models/db"
    24  	git_model "code.gitea.io/gitea/models/git"
    25  	issues_model "code.gitea.io/gitea/models/issues"
    26  	pull_model "code.gitea.io/gitea/models/pull"
    27  	repo_model "code.gitea.io/gitea/models/repo"
    28  	"code.gitea.io/gitea/models/unittest"
    29  	user_model "code.gitea.io/gitea/models/user"
    30  	"code.gitea.io/gitea/models/webhook"
    31  	"code.gitea.io/gitea/modules/git"
    32  	"code.gitea.io/gitea/modules/gitrepo"
    33  	"code.gitea.io/gitea/modules/queue"
    34  	"code.gitea.io/gitea/modules/setting"
    35  	api "code.gitea.io/gitea/modules/structs"
    36  	"code.gitea.io/gitea/modules/test"
    37  	"code.gitea.io/gitea/modules/translation"
    38  	"code.gitea.io/gitea/services/automerge"
    39  	"code.gitea.io/gitea/services/pull"
    40  	repo_service "code.gitea.io/gitea/services/repository"
    41  	commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
    42  	files_service "code.gitea.io/gitea/services/repository/files"
    43  
    44  	"github.com/stretchr/testify/assert"
    45  )
    46  
    47  func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum string, mergeStyle repo_model.MergeStyle, deleteBranch bool) *httptest.ResponseRecorder {
    48  	req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum))
    49  	resp := session.MakeRequest(t, req, http.StatusOK)
    50  
    51  	htmlDoc := NewHTMLParser(t, resp.Body)
    52  	link := path.Join(user, repo, "pulls", pullnum, "merge")
    53  
    54  	options := map[string]string{
    55  		"_csrf": htmlDoc.GetCSRF(),
    56  		"do":    string(mergeStyle),
    57  	}
    58  
    59  	if deleteBranch {
    60  		options["delete_branch_after_merge"] = "on"
    61  	}
    62  
    63  	req = NewRequestWithValues(t, "POST", link, options)
    64  	resp = session.MakeRequest(t, req, http.StatusOK)
    65  
    66  	respJSON := struct {
    67  		Redirect string
    68  	}{}
    69  	DecodeJSON(t, resp, &respJSON)
    70  
    71  	assert.EqualValues(t, fmt.Sprintf("/%s/%s/pulls/%s", user, repo, pullnum), respJSON.Redirect)
    72  
    73  	return resp
    74  }
    75  
    76  func testPullCleanUp(t *testing.T, session *TestSession, user, repo, pullnum string) *httptest.ResponseRecorder {
    77  	req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum))
    78  	resp := session.MakeRequest(t, req, http.StatusOK)
    79  
    80  	// Click the little button to create a pull
    81  	htmlDoc := NewHTMLParser(t, resp.Body)
    82  	link, exists := htmlDoc.doc.Find(".timeline-item .delete-button").Attr("data-url")
    83  	assert.True(t, exists, "The template has changed, can not find delete button url")
    84  	req = NewRequestWithValues(t, "POST", link, map[string]string{
    85  		"_csrf": htmlDoc.GetCSRF(),
    86  	})
    87  	resp = session.MakeRequest(t, req, http.StatusOK)
    88  
    89  	return resp
    90  }
    91  
    92  func TestPullMerge(t *testing.T) {
    93  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
    94  		hookTasks, err := webhook.HookTasks(db.DefaultContext, 1, 1) // Retrieve previous hook number
    95  		assert.NoError(t, err)
    96  		hookTasksLenBefore := len(hookTasks)
    97  
    98  		session := loginUser(t, "user1")
    99  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
   100  		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
   101  
   102  		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
   103  
   104  		elem := strings.Split(test.RedirectURL(resp), "/")
   105  		assert.EqualValues(t, "pulls", elem[3])
   106  		testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false)
   107  
   108  		hookTasks, err = webhook.HookTasks(db.DefaultContext, 1, 1)
   109  		assert.NoError(t, err)
   110  		assert.Len(t, hookTasks, hookTasksLenBefore+1)
   111  	})
   112  }
   113  
   114  func TestPullRebase(t *testing.T) {
   115  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   116  		hookTasks, err := webhook.HookTasks(db.DefaultContext, 1, 1) // Retrieve previous hook number
   117  		assert.NoError(t, err)
   118  		hookTasksLenBefore := len(hookTasks)
   119  
   120  		session := loginUser(t, "user1")
   121  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
   122  		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
   123  
   124  		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
   125  
   126  		elem := strings.Split(test.RedirectURL(resp), "/")
   127  		assert.EqualValues(t, "pulls", elem[3])
   128  		testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebase, false)
   129  
   130  		hookTasks, err = webhook.HookTasks(db.DefaultContext, 1, 1)
   131  		assert.NoError(t, err)
   132  		assert.Len(t, hookTasks, hookTasksLenBefore+1)
   133  	})
   134  }
   135  
   136  func TestPullRebaseMerge(t *testing.T) {
   137  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   138  		hookTasks, err := webhook.HookTasks(db.DefaultContext, 1, 1) // Retrieve previous hook number
   139  		assert.NoError(t, err)
   140  		hookTasksLenBefore := len(hookTasks)
   141  
   142  		session := loginUser(t, "user1")
   143  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
   144  		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
   145  
   146  		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
   147  
   148  		elem := strings.Split(test.RedirectURL(resp), "/")
   149  		assert.EqualValues(t, "pulls", elem[3])
   150  		testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebaseMerge, false)
   151  
   152  		hookTasks, err = webhook.HookTasks(db.DefaultContext, 1, 1)
   153  		assert.NoError(t, err)
   154  		assert.Len(t, hookTasks, hookTasksLenBefore+1)
   155  	})
   156  }
   157  
   158  func TestPullSquash(t *testing.T) {
   159  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   160  		hookTasks, err := webhook.HookTasks(db.DefaultContext, 1, 1) // Retrieve previous hook number
   161  		assert.NoError(t, err)
   162  		hookTasksLenBefore := len(hookTasks)
   163  
   164  		session := loginUser(t, "user1")
   165  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
   166  		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
   167  		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited!)\n")
   168  
   169  		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
   170  
   171  		elem := strings.Split(test.RedirectURL(resp), "/")
   172  		assert.EqualValues(t, "pulls", elem[3])
   173  		testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleSquash, false)
   174  
   175  		hookTasks, err = webhook.HookTasks(db.DefaultContext, 1, 1)
   176  		assert.NoError(t, err)
   177  		assert.Len(t, hookTasks, hookTasksLenBefore+1)
   178  	})
   179  }
   180  
   181  func TestPullCleanUpAfterMerge(t *testing.T) {
   182  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   183  		session := loginUser(t, "user1")
   184  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
   185  		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited - TestPullCleanUpAfterMerge)\n")
   186  
   187  		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "feature/test", "This is a pull title")
   188  
   189  		elem := strings.Split(test.RedirectURL(resp), "/")
   190  		assert.EqualValues(t, "pulls", elem[3])
   191  		testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false)
   192  
   193  		// Check PR branch deletion
   194  		resp = testPullCleanUp(t, session, elem[1], elem[2], elem[4])
   195  		respJSON := struct {
   196  			Redirect string
   197  		}{}
   198  		DecodeJSON(t, resp, &respJSON)
   199  
   200  		assert.NotEmpty(t, respJSON.Redirect, "Redirected URL is not found")
   201  
   202  		elem = strings.Split(respJSON.Redirect, "/")
   203  		assert.EqualValues(t, "pulls", elem[3])
   204  
   205  		// Check branch deletion result
   206  		req := NewRequest(t, "GET", respJSON.Redirect)
   207  		resp = session.MakeRequest(t, req, http.StatusOK)
   208  
   209  		htmlDoc := NewHTMLParser(t, resp.Body)
   210  		resultMsg := htmlDoc.doc.Find(".ui.message>p").Text()
   211  
   212  		assert.EqualValues(t, "Branch \"user1/repo1:feature/test\" has been deleted.", resultMsg)
   213  	})
   214  }
   215  
   216  func TestCantMergeWorkInProgress(t *testing.T) {
   217  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   218  		session := loginUser(t, "user1")
   219  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
   220  		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
   221  
   222  		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "[wip] This is a pull title")
   223  
   224  		req := NewRequest(t, "GET", test.RedirectURL(resp))
   225  		resp = session.MakeRequest(t, req, http.StatusOK)
   226  		htmlDoc := NewHTMLParser(t, resp.Body)
   227  		text := strings.TrimSpace(htmlDoc.doc.Find(".merge-section > .item").Last().Text())
   228  		assert.NotEmpty(t, text, "Can't find WIP text")
   229  
   230  		assert.Contains(t, text, translation.NewLocale("en-US").TrString("repo.pulls.cannot_merge_work_in_progress"), "Unable to find WIP text")
   231  		assert.Contains(t, text, "[wip]", "Unable to find WIP text")
   232  	})
   233  }
   234  
   235  func TestCantMergeConflict(t *testing.T) {
   236  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   237  		session := loginUser(t, "user1")
   238  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
   239  		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
   240  		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
   241  
   242  		// Use API to create a conflicting pr
   243  		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
   244  		req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{
   245  			Head:  "conflict",
   246  			Base:  "base",
   247  			Title: "create a conflicting pr",
   248  		}).AddTokenAuth(token)
   249  		session.MakeRequest(t, req, http.StatusCreated)
   250  
   251  		// Now this PR will be marked conflict - or at least a race will do - so drop down to pure code at this point...
   252  		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
   253  			Name: "user1",
   254  		})
   255  		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
   256  			OwnerID: user1.ID,
   257  			Name:    "repo1",
   258  		})
   259  
   260  		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
   261  			HeadRepoID: repo1.ID,
   262  			BaseRepoID: repo1.ID,
   263  			HeadBranch: "conflict",
   264  			BaseBranch: "base",
   265  		})
   266  
   267  		gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo1)
   268  		assert.NoError(t, err)
   269  
   270  		err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "CONFLICT", false)
   271  		assert.Error(t, err, "Merge should return an error due to conflict")
   272  		assert.True(t, models.IsErrMergeConflicts(err), "Merge error is not a conflict error")
   273  
   274  		err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleRebase, "", "CONFLICT", false)
   275  		assert.Error(t, err, "Merge should return an error due to conflict")
   276  		assert.True(t, models.IsErrRebaseConflicts(err), "Merge error is not a conflict error")
   277  		gitRepo.Close()
   278  	})
   279  }
   280  
   281  func TestCantMergeUnrelated(t *testing.T) {
   282  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   283  		session := loginUser(t, "user1")
   284  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
   285  		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
   286  
   287  		// Now we want to create a commit on a branch that is totally unrelated to our current head
   288  		// Drop down to pure code at this point
   289  		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
   290  			Name: "user1",
   291  		})
   292  		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
   293  			OwnerID: user1.ID,
   294  			Name:    "repo1",
   295  		})
   296  		path := repo_model.RepoPath(user1.Name, repo1.Name)
   297  
   298  		err := git.NewCommand(git.DefaultContext, "read-tree", "--empty").Run(&git.RunOpts{Dir: path})
   299  		assert.NoError(t, err)
   300  
   301  		stdin := bytes.NewBufferString("Unrelated File")
   302  		var stdout strings.Builder
   303  		err = git.NewCommand(git.DefaultContext, "hash-object", "-w", "--stdin").Run(&git.RunOpts{
   304  			Dir:    path,
   305  			Stdin:  stdin,
   306  			Stdout: &stdout,
   307  		})
   308  
   309  		assert.NoError(t, err)
   310  		sha := strings.TrimSpace(stdout.String())
   311  
   312  		_, _, err = git.NewCommand(git.DefaultContext, "update-index", "--add", "--replace", "--cacheinfo").AddDynamicArguments("100644", sha, "somewher-over-the-rainbow").RunStdString(&git.RunOpts{Dir: path})
   313  		assert.NoError(t, err)
   314  
   315  		treeSha, _, err := git.NewCommand(git.DefaultContext, "write-tree").RunStdString(&git.RunOpts{Dir: path})
   316  		assert.NoError(t, err)
   317  		treeSha = strings.TrimSpace(treeSha)
   318  
   319  		commitTimeStr := time.Now().Format(time.RFC3339)
   320  		doerSig := user1.NewGitSig()
   321  		env := append(os.Environ(),
   322  			"GIT_AUTHOR_NAME="+doerSig.Name,
   323  			"GIT_AUTHOR_EMAIL="+doerSig.Email,
   324  			"GIT_AUTHOR_DATE="+commitTimeStr,
   325  			"GIT_COMMITTER_NAME="+doerSig.Name,
   326  			"GIT_COMMITTER_EMAIL="+doerSig.Email,
   327  			"GIT_COMMITTER_DATE="+commitTimeStr,
   328  		)
   329  
   330  		messageBytes := new(bytes.Buffer)
   331  		_, _ = messageBytes.WriteString("Unrelated")
   332  		_, _ = messageBytes.WriteString("\n")
   333  
   334  		stdout.Reset()
   335  		err = git.NewCommand(git.DefaultContext, "commit-tree").AddDynamicArguments(treeSha).
   336  			Run(&git.RunOpts{
   337  				Env:    env,
   338  				Dir:    path,
   339  				Stdin:  messageBytes,
   340  				Stdout: &stdout,
   341  			})
   342  		assert.NoError(t, err)
   343  		commitSha := strings.TrimSpace(stdout.String())
   344  
   345  		_, _, err = git.NewCommand(git.DefaultContext, "branch", "unrelated").AddDynamicArguments(commitSha).RunStdString(&git.RunOpts{Dir: path})
   346  		assert.NoError(t, err)
   347  
   348  		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
   349  
   350  		// Use API to create a conflicting pr
   351  		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
   352  		req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{
   353  			Head:  "unrelated",
   354  			Base:  "base",
   355  			Title: "create an unrelated pr",
   356  		}).AddTokenAuth(token)
   357  		session.MakeRequest(t, req, http.StatusCreated)
   358  
   359  		// Now this PR could be marked conflict - or at least a race may occur - so drop down to pure code at this point...
   360  		gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo1)
   361  		assert.NoError(t, err)
   362  		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
   363  			HeadRepoID: repo1.ID,
   364  			BaseRepoID: repo1.ID,
   365  			HeadBranch: "unrelated",
   366  			BaseBranch: "base",
   367  		})
   368  
   369  		err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "UNRELATED", false)
   370  		assert.Error(t, err, "Merge should return an error due to unrelated")
   371  		assert.True(t, models.IsErrMergeUnrelatedHistories(err), "Merge error is not a unrelated histories error")
   372  		gitRepo.Close()
   373  	})
   374  }
   375  
   376  func TestFastForwardOnlyMerge(t *testing.T) {
   377  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   378  		session := loginUser(t, "user1")
   379  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
   380  		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "update", "README.md", "Hello, World 2\n")
   381  
   382  		// Use API to create a pr from update to master
   383  		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
   384  		req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{
   385  			Head:  "update",
   386  			Base:  "master",
   387  			Title: "create a pr that can be fast-forward-only merged",
   388  		}).AddTokenAuth(token)
   389  		session.MakeRequest(t, req, http.StatusCreated)
   390  
   391  		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
   392  			Name: "user1",
   393  		})
   394  		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
   395  			OwnerID: user1.ID,
   396  			Name:    "repo1",
   397  		})
   398  
   399  		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
   400  			HeadRepoID: repo1.ID,
   401  			BaseRepoID: repo1.ID,
   402  			HeadBranch: "update",
   403  			BaseBranch: "master",
   404  		})
   405  
   406  		gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name))
   407  		assert.NoError(t, err)
   408  
   409  		err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleFastForwardOnly, "", "FAST-FORWARD-ONLY", false)
   410  
   411  		assert.NoError(t, err)
   412  
   413  		gitRepo.Close()
   414  	})
   415  }
   416  
   417  func TestCantFastForwardOnlyMergeDiverging(t *testing.T) {
   418  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   419  		session := loginUser(t, "user1")
   420  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
   421  		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "diverging", "README.md", "Hello, World diverged\n")
   422  		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World 2\n")
   423  
   424  		// Use API to create a pr from diverging to update
   425  		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
   426  		req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{
   427  			Head:  "diverging",
   428  			Base:  "master",
   429  			Title: "create a pr from a diverging branch",
   430  		}).AddTokenAuth(token)
   431  		session.MakeRequest(t, req, http.StatusCreated)
   432  
   433  		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
   434  			Name: "user1",
   435  		})
   436  		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
   437  			OwnerID: user1.ID,
   438  			Name:    "repo1",
   439  		})
   440  
   441  		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
   442  			HeadRepoID: repo1.ID,
   443  			BaseRepoID: repo1.ID,
   444  			HeadBranch: "diverging",
   445  			BaseBranch: "master",
   446  		})
   447  
   448  		gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name))
   449  		assert.NoError(t, err)
   450  
   451  		err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleFastForwardOnly, "", "DIVERGING", false)
   452  
   453  		assert.Error(t, err, "Merge should return an error due to being for a diverging branch")
   454  		assert.True(t, models.IsErrMergeDivergingFastForwardOnly(err), "Merge error is not a diverging fast-forward-only error")
   455  
   456  		gitRepo.Close()
   457  	})
   458  }
   459  
   460  func TestConflictChecking(t *testing.T) {
   461  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   462  		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
   463  
   464  		// Create new clean repo to test conflict checking.
   465  		baseRepo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
   466  			Name:          "conflict-checking",
   467  			Description:   "Tempo repo",
   468  			AutoInit:      true,
   469  			Readme:        "Default",
   470  			DefaultBranch: "main",
   471  		})
   472  		assert.NoError(t, err)
   473  		assert.NotEmpty(t, baseRepo)
   474  
   475  		// create a commit on new branch.
   476  		_, err = files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, user, &files_service.ChangeRepoFilesOptions{
   477  			Files: []*files_service.ChangeRepoFile{
   478  				{
   479  					Operation:     "create",
   480  					TreePath:      "important_file",
   481  					ContentReader: strings.NewReader("Just a non-important file"),
   482  				},
   483  			},
   484  			Message:   "Add a important file",
   485  			OldBranch: "main",
   486  			NewBranch: "important-secrets",
   487  		})
   488  		assert.NoError(t, err)
   489  
   490  		// create a commit on main branch.
   491  		_, err = files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, user, &files_service.ChangeRepoFilesOptions{
   492  			Files: []*files_service.ChangeRepoFile{
   493  				{
   494  					Operation:     "create",
   495  					TreePath:      "important_file",
   496  					ContentReader: strings.NewReader("Not the same content :P"),
   497  				},
   498  			},
   499  			Message:   "Add a important file",
   500  			OldBranch: "main",
   501  			NewBranch: "main",
   502  		})
   503  		assert.NoError(t, err)
   504  
   505  		// create Pull to merge the important-secrets branch into main branch.
   506  		pullIssue := &issues_model.Issue{
   507  			RepoID:   baseRepo.ID,
   508  			Title:    "PR with conflict!",
   509  			PosterID: user.ID,
   510  			Poster:   user,
   511  			IsPull:   true,
   512  		}
   513  
   514  		pullRequest := &issues_model.PullRequest{
   515  			HeadRepoID: baseRepo.ID,
   516  			BaseRepoID: baseRepo.ID,
   517  			HeadBranch: "important-secrets",
   518  			BaseBranch: "main",
   519  			HeadRepo:   baseRepo,
   520  			BaseRepo:   baseRepo,
   521  			Type:       issues_model.PullRequestGitea,
   522  		}
   523  		err = pull.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil)
   524  		assert.NoError(t, err)
   525  
   526  		issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"})
   527  		assert.NoError(t, issue.LoadPullRequest(db.DefaultContext))
   528  		conflictingPR := issue.PullRequest
   529  
   530  		// Ensure conflictedFiles is populated.
   531  		assert.Len(t, conflictingPR.ConflictedFiles, 1)
   532  		// Check if status is correct.
   533  		assert.Equal(t, issues_model.PullRequestStatusConflict, conflictingPR.Status)
   534  		// Ensure that mergeable returns false
   535  		assert.False(t, conflictingPR.Mergeable(db.DefaultContext))
   536  	})
   537  }
   538  
   539  func TestPullRetargetChildOnBranchDelete(t *testing.T) {
   540  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   541  		session := loginUser(t, "user1")
   542  		testEditFileToNewBranch(t, session, "user2", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullRetargetOnCleanup - base PR)\n")
   543  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
   544  		testEditFileToNewBranch(t, session, "user1", "repo1", "base-pr", "child-pr", "README.md", "Hello, World\n(Edited - TestPullRetargetOnCleanup - base PR)\n(Edited - TestPullRetargetOnCleanup - child PR)")
   545  
   546  		respBasePR := testPullCreate(t, session, "user2", "repo1", true, "master", "base-pr", "Base Pull Request")
   547  		elemBasePR := strings.Split(test.RedirectURL(respBasePR), "/")
   548  		assert.EqualValues(t, "pulls", elemBasePR[3])
   549  
   550  		respChildPR := testPullCreate(t, session, "user1", "repo1", false, "base-pr", "child-pr", "Child Pull Request")
   551  		elemChildPR := strings.Split(test.RedirectURL(respChildPR), "/")
   552  		assert.EqualValues(t, "pulls", elemChildPR[3])
   553  
   554  		testPullMerge(t, session, elemBasePR[1], elemBasePR[2], elemBasePR[4], repo_model.MergeStyleMerge, true)
   555  
   556  		// Check child PR
   557  		req := NewRequest(t, "GET", test.RedirectURL(respChildPR))
   558  		resp := session.MakeRequest(t, req, http.StatusOK)
   559  
   560  		htmlDoc := NewHTMLParser(t, resp.Body)
   561  		targetBranch := htmlDoc.doc.Find("#branch_target>a").Text()
   562  		prStatus := strings.TrimSpace(htmlDoc.doc.Find(".issue-title-meta>.issue-state-label").Text())
   563  
   564  		assert.EqualValues(t, "master", targetBranch)
   565  		assert.EqualValues(t, "Open", prStatus)
   566  	})
   567  }
   568  
   569  func TestPullDontRetargetChildOnWrongRepo(t *testing.T) {
   570  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   571  		session := loginUser(t, "user1")
   572  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
   573  		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n")
   574  		testEditFileToNewBranch(t, session, "user1", "repo1", "base-pr", "child-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n(Edited - TestPullDontRetargetChildOnWrongRepo - child PR)")
   575  
   576  		respBasePR := testPullCreate(t, session, "user1", "repo1", false, "master", "base-pr", "Base Pull Request")
   577  		elemBasePR := strings.Split(test.RedirectURL(respBasePR), "/")
   578  		assert.EqualValues(t, "pulls", elemBasePR[3])
   579  
   580  		respChildPR := testPullCreate(t, session, "user1", "repo1", true, "base-pr", "child-pr", "Child Pull Request")
   581  		elemChildPR := strings.Split(test.RedirectURL(respChildPR), "/")
   582  		assert.EqualValues(t, "pulls", elemChildPR[3])
   583  
   584  		testPullMerge(t, session, elemBasePR[1], elemBasePR[2], elemBasePR[4], repo_model.MergeStyleMerge, true)
   585  
   586  		// Check child PR
   587  		req := NewRequest(t, "GET", test.RedirectURL(respChildPR))
   588  		resp := session.MakeRequest(t, req, http.StatusOK)
   589  
   590  		htmlDoc := NewHTMLParser(t, resp.Body)
   591  		targetBranch := htmlDoc.doc.Find("#branch_target>a").Text()
   592  		prStatus := strings.TrimSpace(htmlDoc.doc.Find(".issue-title-meta>.issue-state-label").Text())
   593  
   594  		assert.EqualValues(t, "base-pr", targetBranch)
   595  		assert.EqualValues(t, "Closed", prStatus)
   596  	})
   597  }
   598  
   599  func TestPullMergeIndexerNotifier(t *testing.T) {
   600  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   601  		// create a pull request
   602  		session := loginUser(t, "user1")
   603  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
   604  		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
   605  		createPullResp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "Indexer notifier test pull")
   606  
   607  		assert.NoError(t, queue.GetManager().FlushAll(context.Background(), 0))
   608  		time.Sleep(time.Second)
   609  
   610  		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
   611  			OwnerName: "user2",
   612  			Name:      "repo1",
   613  		})
   614  		issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
   615  			RepoID:   repo1.ID,
   616  			Title:    "Indexer notifier test pull",
   617  			IsPull:   true,
   618  			IsClosed: false,
   619  		})
   620  
   621  		// build the request for searching issues
   622  		link, _ := url.Parse("/api/v1/repos/issues/search")
   623  		query := url.Values{}
   624  		query.Add("state", "closed")
   625  		query.Add("type", "pulls")
   626  		query.Add("q", "notifier")
   627  		link.RawQuery = query.Encode()
   628  
   629  		// search issues
   630  		searchIssuesResp := session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
   631  		var apiIssuesBefore []*api.Issue
   632  		DecodeJSON(t, searchIssuesResp, &apiIssuesBefore)
   633  		assert.Len(t, apiIssuesBefore, 0)
   634  
   635  		// merge the pull request
   636  		elem := strings.Split(test.RedirectURL(createPullResp), "/")
   637  		assert.EqualValues(t, "pulls", elem[3])
   638  		testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false)
   639  
   640  		// check if the issue is closed
   641  		issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
   642  			ID: issue.ID,
   643  		})
   644  		assert.True(t, issue.IsClosed)
   645  
   646  		assert.NoError(t, queue.GetManager().FlushAll(context.Background(), 0))
   647  		time.Sleep(time.Second)
   648  
   649  		// search issues again
   650  		searchIssuesResp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
   651  		var apiIssuesAfter []*api.Issue
   652  		DecodeJSON(t, searchIssuesResp, &apiIssuesAfter)
   653  		if assert.Len(t, apiIssuesAfter, 1) {
   654  			assert.Equal(t, issue.ID, apiIssuesAfter[0].ID)
   655  		}
   656  	})
   657  }
   658  
   659  func testResetRepo(t *testing.T, repoPath, branch, commitID string) {
   660  	f, err := os.OpenFile(filepath.Join(repoPath, "refs", "heads", branch), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
   661  	assert.NoError(t, err)
   662  	_, err = f.WriteString(commitID + "\n")
   663  	assert.NoError(t, err)
   664  	f.Close()
   665  
   666  	repo, err := git.OpenRepository(context.Background(), repoPath)
   667  	assert.NoError(t, err)
   668  	defer repo.Close()
   669  	id, err := repo.GetBranchCommitID(branch)
   670  	assert.NoError(t, err)
   671  	assert.EqualValues(t, commitID, id)
   672  }
   673  
   674  func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) {
   675  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   676  		// create a pull request
   677  		session := loginUser(t, "user1")
   678  		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
   679  		forkedName := "repo1-1"
   680  		testRepoFork(t, session, "user2", "repo1", "user1", forkedName, "")
   681  		defer func() {
   682  			testDeleteRepository(t, session, "user1", forkedName)
   683  		}()
   684  		testEditFile(t, session, "user1", forkedName, "master", "README.md", "Hello, World (Edited)\n")
   685  		testPullCreate(t, session, "user1", forkedName, false, "master", "master", "Indexer notifier test pull")
   686  
   687  		baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
   688  		forkedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: forkedName})
   689  		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
   690  			BaseRepoID: baseRepo.ID,
   691  			BaseBranch: "master",
   692  			HeadRepoID: forkedRepo.ID,
   693  			HeadBranch: "master",
   694  		})
   695  
   696  		// add protected branch for commit status
   697  		csrf := GetCSRF(t, session, "/user2/repo1/settings/branches")
   698  		// Change master branch to protected
   699  		req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
   700  			"_csrf":                 csrf,
   701  			"rule_name":             "master",
   702  			"enable_push":           "true",
   703  			"enable_status_check":   "true",
   704  			"status_check_contexts": "gitea/actions",
   705  		})
   706  		session.MakeRequest(t, req, http.StatusSeeOther)
   707  
   708  		// first time insert automerge record, return true
   709  		scheduled, err := automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test")
   710  		assert.NoError(t, err)
   711  		assert.True(t, scheduled)
   712  
   713  		// second time insert automerge record, return false because it does exist
   714  		scheduled, err = automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test")
   715  		assert.Error(t, err)
   716  		assert.False(t, scheduled)
   717  
   718  		// reload pr again
   719  		pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
   720  		assert.False(t, pr.HasMerged)
   721  		assert.Empty(t, pr.MergedCommitID)
   722  
   723  		// update commit status to success, then it should be merged automatically
   724  		baseGitRepo, err := gitrepo.OpenRepository(db.DefaultContext, baseRepo)
   725  		assert.NoError(t, err)
   726  		sha, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
   727  		assert.NoError(t, err)
   728  		masterCommitID, err := baseGitRepo.GetBranchCommitID("master")
   729  		assert.NoError(t, err)
   730  
   731  		branches, _, err := baseGitRepo.GetBranchNames(0, 100)
   732  		assert.NoError(t, err)
   733  		assert.ElementsMatch(t, []string{"sub-home-md-img-check", "home-md-img-check", "pr-to-update", "branch2", "DefaultBranch", "develop", "feature/1", "master"}, branches)
   734  		baseGitRepo.Close()
   735  		defer func() {
   736  			testResetRepo(t, baseRepo.RepoPath(), "master", masterCommitID)
   737  		}()
   738  
   739  		err = commitstatus_service.CreateCommitStatus(db.DefaultContext, baseRepo, user1, sha, &git_model.CommitStatus{
   740  			State:     api.CommitStatusSuccess,
   741  			TargetURL: "https://gitea.com",
   742  			Context:   "gitea/actions",
   743  		})
   744  		assert.NoError(t, err)
   745  
   746  		time.Sleep(2 * time.Second)
   747  
   748  		// realod pr again
   749  		pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
   750  		assert.True(t, pr.HasMerged)
   751  		assert.NotEmpty(t, pr.MergedCommitID)
   752  
   753  		unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{PullID: pr.ID})
   754  	})
   755  }
   756  
   757  func TestPullAutoMergeAfterCommitStatusSucceedAndApproval(t *testing.T) {
   758  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   759  		// create a pull request
   760  		session := loginUser(t, "user1")
   761  		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
   762  		forkedName := "repo1-2"
   763  		testRepoFork(t, session, "user2", "repo1", "user1", forkedName, "")
   764  		defer func() {
   765  			testDeleteRepository(t, session, "user1", forkedName)
   766  		}()
   767  		testEditFile(t, session, "user1", forkedName, "master", "README.md", "Hello, World (Edited)\n")
   768  		testPullCreate(t, session, "user1", forkedName, false, "master", "master", "Indexer notifier test pull")
   769  
   770  		baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
   771  		forkedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: forkedName})
   772  		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
   773  			BaseRepoID: baseRepo.ID,
   774  			BaseBranch: "master",
   775  			HeadRepoID: forkedRepo.ID,
   776  			HeadBranch: "master",
   777  		})
   778  
   779  		// add protected branch for commit status
   780  		csrf := GetCSRF(t, session, "/user2/repo1/settings/branches")
   781  		// Change master branch to protected
   782  		req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
   783  			"_csrf":                 csrf,
   784  			"rule_name":             "master",
   785  			"enable_push":           "true",
   786  			"enable_status_check":   "true",
   787  			"status_check_contexts": "gitea/actions",
   788  			"required_approvals":    "1",
   789  		})
   790  		session.MakeRequest(t, req, http.StatusSeeOther)
   791  
   792  		// first time insert automerge record, return true
   793  		scheduled, err := automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test")
   794  		assert.NoError(t, err)
   795  		assert.True(t, scheduled)
   796  
   797  		// second time insert automerge record, return false because it does exist
   798  		scheduled, err = automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test")
   799  		assert.Error(t, err)
   800  		assert.False(t, scheduled)
   801  
   802  		// reload pr again
   803  		pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
   804  		assert.False(t, pr.HasMerged)
   805  		assert.Empty(t, pr.MergedCommitID)
   806  
   807  		// update commit status to success, then it should be merged automatically
   808  		baseGitRepo, err := gitrepo.OpenRepository(db.DefaultContext, baseRepo)
   809  		assert.NoError(t, err)
   810  		sha, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
   811  		assert.NoError(t, err)
   812  		masterCommitID, err := baseGitRepo.GetBranchCommitID("master")
   813  		assert.NoError(t, err)
   814  		baseGitRepo.Close()
   815  		defer func() {
   816  			testResetRepo(t, baseRepo.RepoPath(), "master", masterCommitID)
   817  		}()
   818  
   819  		err = commitstatus_service.CreateCommitStatus(db.DefaultContext, baseRepo, user1, sha, &git_model.CommitStatus{
   820  			State:     api.CommitStatusSuccess,
   821  			TargetURL: "https://gitea.com",
   822  			Context:   "gitea/actions",
   823  		})
   824  		assert.NoError(t, err)
   825  
   826  		time.Sleep(2 * time.Second)
   827  
   828  		// reload pr again
   829  		pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
   830  		assert.False(t, pr.HasMerged)
   831  		assert.Empty(t, pr.MergedCommitID)
   832  
   833  		// approve the PR from non-author
   834  		approveSession := loginUser(t, "user2")
   835  		req = NewRequest(t, "GET", fmt.Sprintf("/user2/repo1/pulls/%d", pr.Index))
   836  		resp := approveSession.MakeRequest(t, req, http.StatusOK)
   837  		htmlDoc := NewHTMLParser(t, resp.Body)
   838  		testSubmitReview(t, approveSession, htmlDoc.GetCSRF(), "user2", "repo1", strconv.Itoa(int(pr.Index)), sha, "approve", http.StatusOK)
   839  
   840  		time.Sleep(2 * time.Second)
   841  
   842  		// realod pr again
   843  		pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
   844  		assert.True(t, pr.HasMerged)
   845  		assert.NotEmpty(t, pr.MergedCommitID)
   846  
   847  		unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{PullID: pr.ID})
   848  	})
   849  }
   850  
   851  func TestPullAutoMergeAfterCommitStatusSucceedAndApprovalForAgitFlow(t *testing.T) {
   852  	onGiteaRun(t, func(t *testing.T, u *url.URL) {
   853  		// create a pull request
   854  		baseAPITestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
   855  
   856  		dstPath := t.TempDir()
   857  
   858  		u.Path = baseAPITestContext.GitPath()
   859  		u.User = url.UserPassword("user2", userPassword)
   860  
   861  		t.Run("Clone", doGitClone(dstPath, u))
   862  
   863  		err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content"), 0o666)
   864  		assert.NoError(t, err)
   865  
   866  		err = git.AddChanges(dstPath, true)
   867  		assert.NoError(t, err)
   868  
   869  		err = git.CommitChanges(dstPath, git.CommitChangesOptions{
   870  			Committer: &git.Signature{
   871  				Email: "user2@example.com",
   872  				Name:  "user2",
   873  				When:  time.Now(),
   874  			},
   875  			Author: &git.Signature{
   876  				Email: "user2@example.com",
   877  				Name:  "user2",
   878  				When:  time.Now(),
   879  			},
   880  			Message: "Testing commit 1",
   881  		})
   882  		assert.NoError(t, err)
   883  
   884  		stderrBuf := &bytes.Buffer{}
   885  
   886  		err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o").
   887  			AddDynamicArguments(`topic=test/head2`).
   888  			AddArguments("-o").
   889  			AddDynamicArguments(`title="create a test pull request with agit"`).
   890  			AddArguments("-o").
   891  			AddDynamicArguments(`description="This PR is a test pull request which created with agit"`).
   892  			Run(&git.RunOpts{Dir: dstPath, Stderr: stderrBuf})
   893  		assert.NoError(t, err)
   894  
   895  		assert.Contains(t, stderrBuf.String(), setting.AppURL+"user2/repo1/pulls/6")
   896  
   897  		baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
   898  		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
   899  			Flow:       issues_model.PullRequestFlowAGit,
   900  			BaseRepoID: baseRepo.ID,
   901  			BaseBranch: "master",
   902  			HeadRepoID: baseRepo.ID,
   903  			HeadBranch: "user2/test/head2",
   904  		})
   905  
   906  		session := loginUser(t, "user1")
   907  		// add protected branch for commit status
   908  		csrf := GetCSRF(t, session, "/user2/repo1/settings/branches")
   909  		// Change master branch to protected
   910  		req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
   911  			"_csrf":                 csrf,
   912  			"rule_name":             "master",
   913  			"enable_push":           "true",
   914  			"enable_status_check":   "true",
   915  			"status_check_contexts": "gitea/actions",
   916  			"required_approvals":    "1",
   917  		})
   918  		session.MakeRequest(t, req, http.StatusSeeOther)
   919  
   920  		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
   921  		// first time insert automerge record, return true
   922  		scheduled, err := automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test")
   923  		assert.NoError(t, err)
   924  		assert.True(t, scheduled)
   925  
   926  		// second time insert automerge record, return false because it does exist
   927  		scheduled, err = automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test")
   928  		assert.Error(t, err)
   929  		assert.False(t, scheduled)
   930  
   931  		// reload pr again
   932  		pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
   933  		assert.False(t, pr.HasMerged)
   934  		assert.Empty(t, pr.MergedCommitID)
   935  
   936  		// update commit status to success, then it should be merged automatically
   937  		baseGitRepo, err := gitrepo.OpenRepository(db.DefaultContext, baseRepo)
   938  		assert.NoError(t, err)
   939  		sha, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
   940  		assert.NoError(t, err)
   941  		masterCommitID, err := baseGitRepo.GetBranchCommitID("master")
   942  		assert.NoError(t, err)
   943  		baseGitRepo.Close()
   944  		defer func() {
   945  			testResetRepo(t, baseRepo.RepoPath(), "master", masterCommitID)
   946  		}()
   947  
   948  		err = commitstatus_service.CreateCommitStatus(db.DefaultContext, baseRepo, user1, sha, &git_model.CommitStatus{
   949  			State:     api.CommitStatusSuccess,
   950  			TargetURL: "https://gitea.com",
   951  			Context:   "gitea/actions",
   952  		})
   953  		assert.NoError(t, err)
   954  
   955  		time.Sleep(2 * time.Second)
   956  
   957  		// reload pr again
   958  		pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
   959  		assert.False(t, pr.HasMerged)
   960  		assert.Empty(t, pr.MergedCommitID)
   961  
   962  		// approve the PR from non-author
   963  		approveSession := loginUser(t, "user1")
   964  		req = NewRequest(t, "GET", fmt.Sprintf("/user2/repo1/pulls/%d", pr.Index))
   965  		resp := approveSession.MakeRequest(t, req, http.StatusOK)
   966  		htmlDoc := NewHTMLParser(t, resp.Body)
   967  		testSubmitReview(t, approveSession, htmlDoc.GetCSRF(), "user2", "repo1", strconv.Itoa(int(pr.Index)), sha, "approve", http.StatusOK)
   968  
   969  		time.Sleep(2 * time.Second)
   970  
   971  		// realod pr again
   972  		pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
   973  		assert.True(t, pr.HasMerged)
   974  		assert.NotEmpty(t, pr.MergedCommitID)
   975  
   976  		unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{PullID: pr.ID})
   977  	})
   978  }