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 }