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 }