code.gitea.io/gitea@v1.21.7/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  	"strings"
    16  	"testing"
    17  	"time"
    18  
    19  	"code.gitea.io/gitea/models"
    20  	auth_model "code.gitea.io/gitea/models/auth"
    21  	"code.gitea.io/gitea/models/db"
    22  	issues_model "code.gitea.io/gitea/models/issues"
    23  	repo_model "code.gitea.io/gitea/models/repo"
    24  	"code.gitea.io/gitea/models/unittest"
    25  	user_model "code.gitea.io/gitea/models/user"
    26  	"code.gitea.io/gitea/models/webhook"
    27  	"code.gitea.io/gitea/modules/git"
    28  	api "code.gitea.io/gitea/modules/structs"
    29  	"code.gitea.io/gitea/modules/test"
    30  	"code.gitea.io/gitea/modules/translation"
    31  	"code.gitea.io/gitea/services/pull"
    32  	repo_service "code.gitea.io/gitea/services/repository"
    33  	files_service "code.gitea.io/gitea/services/repository/files"
    34  
    35  	"github.com/stretchr/testify/assert"
    36  )
    37  
    38  func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum string, mergeStyle repo_model.MergeStyle) *httptest.ResponseRecorder {
    39  	req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum))
    40  	resp := session.MakeRequest(t, req, http.StatusOK)
    41  
    42  	htmlDoc := NewHTMLParser(t, resp.Body)
    43  	link := path.Join(user, repo, "pulls", pullnum, "merge")
    44  	req = NewRequestWithValues(t, "POST", link, map[string]string{
    45  		"_csrf": htmlDoc.GetCSRF(),
    46  		"do":    string(mergeStyle),
    47  	})
    48  	resp = session.MakeRequest(t, req, http.StatusOK)
    49  
    50  	respJSON := struct {
    51  		Redirect string
    52  	}{}
    53  	DecodeJSON(t, resp, &respJSON)
    54  
    55  	assert.EqualValues(t, fmt.Sprintf("/%s/%s/pulls/%s", user, repo, pullnum), respJSON.Redirect)
    56  
    57  	return resp
    58  }
    59  
    60  func testPullCleanUp(t *testing.T, session *TestSession, user, repo, pullnum string) *httptest.ResponseRecorder {
    61  	req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum))
    62  	resp := session.MakeRequest(t, req, http.StatusOK)
    63  
    64  	// Click the little button to create a pull
    65  	htmlDoc := NewHTMLParser(t, resp.Body)
    66  	link, exists := htmlDoc.doc.Find(".timeline-item .delete-button").Attr("data-url")
    67  	assert.True(t, exists, "The template has changed, can not find delete button url")
    68  	req = NewRequestWithValues(t, "POST", link, map[string]string{
    69  		"_csrf": htmlDoc.GetCSRF(),
    70  	})
    71  	resp = session.MakeRequest(t, req, http.StatusOK)
    72  
    73  	return resp
    74  }
    75  
    76  func TestPullMerge(t *testing.T) {
    77  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
    78  		hookTasks, err := webhook.HookTasks(1, 1) // Retrieve previous hook number
    79  		assert.NoError(t, err)
    80  		hookTasksLenBefore := len(hookTasks)
    81  
    82  		session := loginUser(t, "user1")
    83  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
    84  		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
    85  
    86  		resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
    87  
    88  		elem := strings.Split(test.RedirectURL(resp), "/")
    89  		assert.EqualValues(t, "pulls", elem[3])
    90  		testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge)
    91  
    92  		hookTasks, err = webhook.HookTasks(1, 1)
    93  		assert.NoError(t, err)
    94  		assert.Len(t, hookTasks, hookTasksLenBefore+1)
    95  	})
    96  }
    97  
    98  func TestPullRebase(t *testing.T) {
    99  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   100  		hookTasks, err := webhook.HookTasks(1, 1) // Retrieve previous hook number
   101  		assert.NoError(t, err)
   102  		hookTasksLenBefore := len(hookTasks)
   103  
   104  		session := loginUser(t, "user1")
   105  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
   106  		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
   107  
   108  		resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
   109  
   110  		elem := strings.Split(test.RedirectURL(resp), "/")
   111  		assert.EqualValues(t, "pulls", elem[3])
   112  		testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebase)
   113  
   114  		hookTasks, err = webhook.HookTasks(1, 1)
   115  		assert.NoError(t, err)
   116  		assert.Len(t, hookTasks, hookTasksLenBefore+1)
   117  	})
   118  }
   119  
   120  func TestPullRebaseMerge(t *testing.T) {
   121  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   122  		hookTasks, err := webhook.HookTasks(1, 1) // Retrieve previous hook number
   123  		assert.NoError(t, err)
   124  		hookTasksLenBefore := len(hookTasks)
   125  
   126  		session := loginUser(t, "user1")
   127  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
   128  		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
   129  
   130  		resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
   131  
   132  		elem := strings.Split(test.RedirectURL(resp), "/")
   133  		assert.EqualValues(t, "pulls", elem[3])
   134  		testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebaseMerge)
   135  
   136  		hookTasks, err = webhook.HookTasks(1, 1)
   137  		assert.NoError(t, err)
   138  		assert.Len(t, hookTasks, hookTasksLenBefore+1)
   139  	})
   140  }
   141  
   142  func TestPullSquash(t *testing.T) {
   143  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   144  		hookTasks, err := webhook.HookTasks(1, 1) // Retrieve previous hook number
   145  		assert.NoError(t, err)
   146  		hookTasksLenBefore := len(hookTasks)
   147  
   148  		session := loginUser(t, "user1")
   149  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
   150  		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
   151  		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited!)\n")
   152  
   153  		resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
   154  
   155  		elem := strings.Split(test.RedirectURL(resp), "/")
   156  		assert.EqualValues(t, "pulls", elem[3])
   157  		testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleSquash)
   158  
   159  		hookTasks, err = webhook.HookTasks(1, 1)
   160  		assert.NoError(t, err)
   161  		assert.Len(t, hookTasks, hookTasksLenBefore+1)
   162  	})
   163  }
   164  
   165  func TestPullCleanUpAfterMerge(t *testing.T) {
   166  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   167  		session := loginUser(t, "user1")
   168  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
   169  		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited - TestPullCleanUpAfterMerge)\n")
   170  
   171  		resp := testPullCreate(t, session, "user1", "repo1", "feature/test", "This is a pull title")
   172  
   173  		elem := strings.Split(test.RedirectURL(resp), "/")
   174  		assert.EqualValues(t, "pulls", elem[3])
   175  		testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge)
   176  
   177  		// Check PR branch deletion
   178  		resp = testPullCleanUp(t, session, elem[1], elem[2], elem[4])
   179  		respJSON := struct {
   180  			Redirect string
   181  		}{}
   182  		DecodeJSON(t, resp, &respJSON)
   183  
   184  		assert.NotEmpty(t, respJSON.Redirect, "Redirected URL is not found")
   185  
   186  		elem = strings.Split(respJSON.Redirect, "/")
   187  		assert.EqualValues(t, "pulls", elem[3])
   188  
   189  		// Check branch deletion result
   190  		req := NewRequest(t, "GET", respJSON.Redirect)
   191  		resp = session.MakeRequest(t, req, http.StatusOK)
   192  
   193  		htmlDoc := NewHTMLParser(t, resp.Body)
   194  		resultMsg := htmlDoc.doc.Find(".ui.message>p").Text()
   195  
   196  		assert.EqualValues(t, "Branch \"user1/repo1:feature/test\" has been deleted.", resultMsg)
   197  	})
   198  }
   199  
   200  func TestCantMergeWorkInProgress(t *testing.T) {
   201  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   202  		session := loginUser(t, "user1")
   203  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
   204  		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
   205  
   206  		resp := testPullCreate(t, session, "user1", "repo1", "master", "[wip] This is a pull title")
   207  
   208  		req := NewRequest(t, "GET", test.RedirectURL(resp))
   209  		resp = session.MakeRequest(t, req, http.StatusOK)
   210  		htmlDoc := NewHTMLParser(t, resp.Body)
   211  		text := strings.TrimSpace(htmlDoc.doc.Find(".merge-section > .item").Last().Text())
   212  		assert.NotEmpty(t, text, "Can't find WIP text")
   213  
   214  		assert.Contains(t, text, translation.NewLocale("en-US").Tr("repo.pulls.cannot_merge_work_in_progress"), "Unable to find WIP text")
   215  		assert.Contains(t, text, "[wip]", "Unable to find WIP text")
   216  	})
   217  }
   218  
   219  func TestCantMergeConflict(t *testing.T) {
   220  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   221  		session := loginUser(t, "user1")
   222  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
   223  		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
   224  		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
   225  
   226  		// Use API to create a conflicting pr
   227  		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
   228  		req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", "user1", "repo1", token), &api.CreatePullRequestOption{
   229  			Head:  "conflict",
   230  			Base:  "base",
   231  			Title: "create a conflicting pr",
   232  		})
   233  		session.MakeRequest(t, req, http.StatusCreated)
   234  
   235  		// Now this PR will be marked conflict - or at least a race will do - so drop down to pure code at this point...
   236  		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
   237  			Name: "user1",
   238  		})
   239  		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
   240  			OwnerID: user1.ID,
   241  			Name:    "repo1",
   242  		})
   243  
   244  		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
   245  			HeadRepoID: repo1.ID,
   246  			BaseRepoID: repo1.ID,
   247  			HeadBranch: "conflict",
   248  			BaseBranch: "base",
   249  		})
   250  
   251  		gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name))
   252  		assert.NoError(t, err)
   253  
   254  		err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "CONFLICT", false)
   255  		assert.Error(t, err, "Merge should return an error due to conflict")
   256  		assert.True(t, models.IsErrMergeConflicts(err), "Merge error is not a conflict error")
   257  
   258  		err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleRebase, "", "CONFLICT", false)
   259  		assert.Error(t, err, "Merge should return an error due to conflict")
   260  		assert.True(t, models.IsErrRebaseConflicts(err), "Merge error is not a conflict error")
   261  		gitRepo.Close()
   262  	})
   263  }
   264  
   265  func TestCantMergeUnrelated(t *testing.T) {
   266  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   267  		session := loginUser(t, "user1")
   268  		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
   269  		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
   270  
   271  		// Now we want to create a commit on a branch that is totally unrelated to our current head
   272  		// Drop down to pure code at this point
   273  		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
   274  			Name: "user1",
   275  		})
   276  		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
   277  			OwnerID: user1.ID,
   278  			Name:    "repo1",
   279  		})
   280  		path := repo_model.RepoPath(user1.Name, repo1.Name)
   281  
   282  		err := git.NewCommand(git.DefaultContext, "read-tree", "--empty").Run(&git.RunOpts{Dir: path})
   283  		assert.NoError(t, err)
   284  
   285  		stdin := bytes.NewBufferString("Unrelated File")
   286  		var stdout strings.Builder
   287  		err = git.NewCommand(git.DefaultContext, "hash-object", "-w", "--stdin").Run(&git.RunOpts{
   288  			Dir:    path,
   289  			Stdin:  stdin,
   290  			Stdout: &stdout,
   291  		})
   292  
   293  		assert.NoError(t, err)
   294  		sha := strings.TrimSpace(stdout.String())
   295  
   296  		_, _, err = git.NewCommand(git.DefaultContext, "update-index", "--add", "--replace", "--cacheinfo").AddDynamicArguments("100644", sha, "somewher-over-the-rainbow").RunStdString(&git.RunOpts{Dir: path})
   297  		assert.NoError(t, err)
   298  
   299  		treeSha, _, err := git.NewCommand(git.DefaultContext, "write-tree").RunStdString(&git.RunOpts{Dir: path})
   300  		assert.NoError(t, err)
   301  		treeSha = strings.TrimSpace(treeSha)
   302  
   303  		commitTimeStr := time.Now().Format(time.RFC3339)
   304  		doerSig := user1.NewGitSig()
   305  		env := append(os.Environ(),
   306  			"GIT_AUTHOR_NAME="+doerSig.Name,
   307  			"GIT_AUTHOR_EMAIL="+doerSig.Email,
   308  			"GIT_AUTHOR_DATE="+commitTimeStr,
   309  			"GIT_COMMITTER_NAME="+doerSig.Name,
   310  			"GIT_COMMITTER_EMAIL="+doerSig.Email,
   311  			"GIT_COMMITTER_DATE="+commitTimeStr,
   312  		)
   313  
   314  		messageBytes := new(bytes.Buffer)
   315  		_, _ = messageBytes.WriteString("Unrelated")
   316  		_, _ = messageBytes.WriteString("\n")
   317  
   318  		stdout.Reset()
   319  		err = git.NewCommand(git.DefaultContext, "commit-tree").AddDynamicArguments(treeSha).
   320  			Run(&git.RunOpts{
   321  				Env:    env,
   322  				Dir:    path,
   323  				Stdin:  messageBytes,
   324  				Stdout: &stdout,
   325  			})
   326  		assert.NoError(t, err)
   327  		commitSha := strings.TrimSpace(stdout.String())
   328  
   329  		_, _, err = git.NewCommand(git.DefaultContext, "branch", "unrelated").AddDynamicArguments(commitSha).RunStdString(&git.RunOpts{Dir: path})
   330  		assert.NoError(t, err)
   331  
   332  		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
   333  
   334  		// Use API to create a conflicting pr
   335  		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
   336  		req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", "user1", "repo1", token), &api.CreatePullRequestOption{
   337  			Head:  "unrelated",
   338  			Base:  "base",
   339  			Title: "create an unrelated pr",
   340  		})
   341  		session.MakeRequest(t, req, http.StatusCreated)
   342  
   343  		// Now this PR could be marked conflict - or at least a race may occur - so drop down to pure code at this point...
   344  		gitRepo, err := git.OpenRepository(git.DefaultContext, path)
   345  		assert.NoError(t, err)
   346  		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
   347  			HeadRepoID: repo1.ID,
   348  			BaseRepoID: repo1.ID,
   349  			HeadBranch: "unrelated",
   350  			BaseBranch: "base",
   351  		})
   352  
   353  		err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "UNRELATED", false)
   354  		assert.Error(t, err, "Merge should return an error due to unrelated")
   355  		assert.True(t, models.IsErrMergeUnrelatedHistories(err), "Merge error is not a unrelated histories error")
   356  		gitRepo.Close()
   357  	})
   358  }
   359  
   360  func TestConflictChecking(t *testing.T) {
   361  	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
   362  		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
   363  
   364  		// Create new clean repo to test conflict checking.
   365  		baseRepo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
   366  			Name:          "conflict-checking",
   367  			Description:   "Tempo repo",
   368  			AutoInit:      true,
   369  			Readme:        "Default",
   370  			DefaultBranch: "main",
   371  		})
   372  		assert.NoError(t, err)
   373  		assert.NotEmpty(t, baseRepo)
   374  
   375  		// create a commit on new branch.
   376  		_, err = files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, user, &files_service.ChangeRepoFilesOptions{
   377  			Files: []*files_service.ChangeRepoFile{
   378  				{
   379  					Operation:     "create",
   380  					TreePath:      "important_file",
   381  					ContentReader: strings.NewReader("Just a non-important file"),
   382  				},
   383  			},
   384  			Message:   "Add a important file",
   385  			OldBranch: "main",
   386  			NewBranch: "important-secrets",
   387  		})
   388  		assert.NoError(t, err)
   389  
   390  		// create a commit on main branch.
   391  		_, err = files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, user, &files_service.ChangeRepoFilesOptions{
   392  			Files: []*files_service.ChangeRepoFile{
   393  				{
   394  					Operation:     "create",
   395  					TreePath:      "important_file",
   396  					ContentReader: strings.NewReader("Not the same content :P"),
   397  				},
   398  			},
   399  			Message:   "Add a important file",
   400  			OldBranch: "main",
   401  			NewBranch: "main",
   402  		})
   403  		assert.NoError(t, err)
   404  
   405  		// create Pull to merge the important-secrets branch into main branch.
   406  		pullIssue := &issues_model.Issue{
   407  			RepoID:   baseRepo.ID,
   408  			Title:    "PR with conflict!",
   409  			PosterID: user.ID,
   410  			Poster:   user,
   411  			IsPull:   true,
   412  		}
   413  
   414  		pullRequest := &issues_model.PullRequest{
   415  			HeadRepoID: baseRepo.ID,
   416  			BaseRepoID: baseRepo.ID,
   417  			HeadBranch: "important-secrets",
   418  			BaseBranch: "main",
   419  			HeadRepo:   baseRepo,
   420  			BaseRepo:   baseRepo,
   421  			Type:       issues_model.PullRequestGitea,
   422  		}
   423  		err = pull.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil)
   424  		assert.NoError(t, err)
   425  
   426  		issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"})
   427  		conflictingPR, err := issues_model.GetPullRequestByIssueID(db.DefaultContext, issue.ID)
   428  		assert.NoError(t, err)
   429  
   430  		// Ensure conflictedFiles is populated.
   431  		assert.Len(t, conflictingPR.ConflictedFiles, 1)
   432  		// Check if status is correct.
   433  		assert.Equal(t, issues_model.PullRequestStatusConflict, conflictingPR.Status)
   434  		// Ensure that mergeable returns false
   435  		assert.False(t, conflictingPR.Mergeable())
   436  	})
   437  }