mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-07-10 05:59:21 +02:00
[v11.0/forgejo] fix: do not ignore automerge while a PR is checking for conflicts (#8456)
**Backport: https://codeberg.org/forgejo/forgejo/pulls/8189** This is a clean cherry-pick codewise. There was a minor conflict to resolve in the tests: ``` Conflicts: tests/integration/patch_status_test.go the test file did not exist at all in v11 and was added as is with one exception: `ObjectFormat: optional.Some("sha256"),` was commented out. ``` --- Automerge can be ignored when the following race happens: * Conflict check is happening on a repository and `pr.Status = issues_model.PullRequestStatusChecking` for all open pull requests (this happens every time a pull request is merged). * While the conflict check is ongoing, an event (Forgejo Actions being successful for instance) happens and and `StartPRCheckAndAutoMerge*` is called. * Because `pr.CanAutoMerge()` is false, the pull request is not selected and not added to the automerge queue. * When the conflict check completes and `pr.CanAutoMerge()` becomes true, there no longer is a task in the auto merge queue and the auto merge does not happen. This is fixed by adding a task to the auto merge queue when the conflict check for a pull request completes. This is done when the mutx protecting the conflict check task is released to prevent a deadlock when a synchronous queues are used in the following situation: * the conflict check task finds the pull request is mergeable * it schedules the auto merge tasks that finds it must be merged * merging concludes with scheduling a conflict check task Avoid an extra loop where a conflict check task queues an auto merge task that will schedule a conflict check task if the pull request can be merged. The auto merge row is removed from the database before merging. It would otherwise be removed after the merge commit is received via the git hook which happens asynchronously and can lead to a race. StartPRCheckAndAutoMerge is modified to re-use HeadCommitID when available, such as when called after a pull request conflict check. --- A note on tests: they cover the new behavior, i.e. automerge being triggered by a successful conflict check. This is also on the critical paths for every test that involve creating, merging or updating a pull request. - `tests/integration/git_test.go` - `tests/integration/actions_commit_status_test.go` - `tests/integration/api_helper_for_declarative_test.go` - `tests/integration/patch_status_test.go` - `tests/integration/pull_merge_test.go` The [missing fixture file](https://codeberg.org/forgejo/forgejo/pulls/8189/files#diff-b86fdd79108b3ba3cb2e56ffcfd1be2a7b32f46c) for the auto merge table can be verified to be necessary simply by removing it an observing that the integration tests fail. The [scheduling of the auto merge task](https://codeberg.org/forgejo/forgejo/pulls/8189/files#diff-9489262e93967f6bb2db41837f37c06f4e70d978) in `testPR` can be verified to be required by moving it in the `testPRProtected` function and observing that the tests hang forever because of the deadlock. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I added test coverage for JavaScript changes... - [ ] in `web_src/js/*.test.js` if it can be unit tested. - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] I do not want this change to show in the release notes. - [x] I want the title to show in the release notes with a link to this pull request. - [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title. <!--start release-notes-assistant--> ## Release notes <!--URL:https://codeberg.org/forgejo/forgejo--> - Bug fixes - [PR](https://codeberg.org/forgejo/forgejo/pulls/8189): <!--number 8189 --><!--line 0 --><!--description ZG8gbm90IGlnbm9yZSBhdXRvbWVyZ2Ugd2hpbGUgYSBQUiBpcyBjaGVja2luZyBmb3IgY29uZmxpY3Rz-->do not ignore automerge while a PR is checking for conflicts<!--description--> <!--end release-notes-assistant--> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8456 Reviewed-by: Lucas <sclu1034@noreply.codeberg.org> Co-authored-by: Earl Warren <contact@earl-warren.org> Co-committed-by: Earl Warren <contact@earl-warren.org>
This commit is contained in:
parent
094eb89b5e
commit
cfea452aa9
6 changed files with 413 additions and 25 deletions
1
models/fixtures/pull_auto_merge.yml
Normal file
1
models/fixtures/pull_auto_merge.yml
Normal file
|
@ -0,0 +1 @@
|
|||
[] # empty
|
|
@ -10,6 +10,7 @@ import (
|
|||
"forgejo.org/models/db"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/timeutil"
|
||||
)
|
||||
|
||||
|
@ -58,13 +59,15 @@ func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pullID int64,
|
|||
return ErrAlreadyScheduledToAutoMerge{PullID: pullID}
|
||||
}
|
||||
|
||||
_, err := db.GetEngine(ctx).Insert(&AutoMerge{
|
||||
scheduledPRM, err := db.GetEngine(ctx).Insert(&AutoMerge{
|
||||
DoerID: doer.ID,
|
||||
PullID: pullID,
|
||||
MergeStyle: style,
|
||||
Message: message,
|
||||
DeleteBranchAfterMerge: deleteBranch,
|
||||
})
|
||||
log.Trace("ScheduleAutoMerge %+v for PR %d", scheduledPRM, pullID)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -81,6 +84,8 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe
|
|||
return false, nil, err
|
||||
}
|
||||
|
||||
log.Trace("GetScheduledMergeByPullID found %+v for PR %d", scheduledPRM, pullID)
|
||||
|
||||
scheduledPRM.Doer = doer
|
||||
return true, scheduledPRM, nil
|
||||
}
|
||||
|
@ -94,6 +99,8 @@ func DeleteScheduledAutoMerge(ctx context.Context, pullID int64) error {
|
|||
return db.ErrNotExist{Resource: "auto_merge", ID: pullID}
|
||||
}
|
||||
|
||||
log.Trace("DeleteScheduledAutoMerge %+v for PR %d", scheduledPRM, pullID)
|
||||
|
||||
_, err = db.GetEngine(ctx).ID(scheduledPRM.ID).Delete(&AutoMerge{})
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -107,6 +107,7 @@ func handlePullRequestAutoMerge(pullID int64, sha string) {
|
|||
return
|
||||
}
|
||||
if !exists {
|
||||
log.Trace("GetScheduledMergeByPullID found nothing for PR %d", pullID)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -204,6 +205,10 @@ func handlePullRequestAutoMerge(pullID int64, sha string) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := pull_model.DeleteScheduledAutoMerge(ctx, pr.ID); err != nil && !db.IsErrNotExist(err) {
|
||||
log.Error("DeleteScheduledAutoMerge[%d]: %v", pr.ID, err)
|
||||
}
|
||||
|
||||
if err := pull_service.Merge(ctx, pr, doer, baseGitRepo, scheduledPRM.MergeStyle, "", scheduledPRM.Message, true); err != nil {
|
||||
log.Error("pull_service.Merge: %v", err)
|
||||
// FIXME: if merge failed, we should display some error message to the pull request page.
|
||||
|
|
|
@ -28,6 +28,7 @@ import (
|
|||
"forgejo.org/modules/timeutil"
|
||||
asymkey_service "forgejo.org/services/asymkey"
|
||||
notify_service "forgejo.org/services/notify"
|
||||
shared_automerge "forgejo.org/services/shared/automerge"
|
||||
)
|
||||
|
||||
// prPatchCheckerQueue represents a queue to handle update pull request tests
|
||||
|
@ -170,7 +171,7 @@ func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer
|
|||
|
||||
// checkAndUpdateStatus checks if pull request is possible to leaving checking status,
|
||||
// and set to be either conflict or mergeable.
|
||||
func checkAndUpdateStatus(ctx context.Context, pr *issues_model.PullRequest) {
|
||||
func checkAndUpdateStatus(ctx context.Context, pr *issues_model.PullRequest) bool {
|
||||
// If status has not been changed to conflict by testPatch then we are mergeable
|
||||
if pr.Status == issues_model.PullRequestStatusChecking {
|
||||
pr.Status = issues_model.PullRequestStatusMergeable
|
||||
|
@ -184,12 +185,15 @@ func checkAndUpdateStatus(ctx context.Context, pr *issues_model.PullRequest) {
|
|||
|
||||
if has {
|
||||
log.Trace("Not updating status for %-v as it is due to be rechecked", pr)
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
if err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files"); err != nil {
|
||||
log.Error("Update[%-v]: %v", pr, err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// getMergeCommit checks if a pull request has been merged
|
||||
|
@ -339,15 +343,22 @@ func handler(items ...string) []string {
|
|||
}
|
||||
|
||||
func testPR(id int64) {
|
||||
pullWorkingPool.CheckIn(fmt.Sprint(id))
|
||||
defer pullWorkingPool.CheckOut(fmt.Sprint(id))
|
||||
ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("Test PR[%d] from patch checking queue", id))
|
||||
defer finished()
|
||||
|
||||
if pr, updated := testPRProtected(ctx, id); pr != nil && updated {
|
||||
shared_automerge.AddToQueueIfMergeable(ctx, pr)
|
||||
}
|
||||
}
|
||||
|
||||
func testPRProtected(ctx context.Context, id int64) (*issues_model.PullRequest, bool) {
|
||||
pullWorkingPool.CheckIn(fmt.Sprint(id))
|
||||
defer pullWorkingPool.CheckOut(fmt.Sprint(id))
|
||||
|
||||
pr, err := issues_model.GetPullRequestByID(ctx, id)
|
||||
if err != nil {
|
||||
log.Error("Unable to GetPullRequestByID[%d] for testPR: %v", id, err)
|
||||
return
|
||||
return nil, false
|
||||
}
|
||||
|
||||
log.Trace("Testing %-v", pr)
|
||||
|
@ -357,12 +368,12 @@ func testPR(id int64) {
|
|||
|
||||
if pr.HasMerged {
|
||||
log.Trace("%-v is already merged (status: %s, merge commit: %s)", pr, pr.Status, pr.MergedCommitID)
|
||||
return
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if manuallyMerged(ctx, pr) {
|
||||
log.Trace("%-v is manually merged (status: %s, merge commit: %s)", pr, pr.Status, pr.MergedCommitID)
|
||||
return
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if err := TestPatch(pr); err != nil {
|
||||
|
@ -371,9 +382,10 @@ func testPR(id int64) {
|
|||
if err := pr.UpdateCols(ctx, "status"); err != nil {
|
||||
log.Error("update pr [%-v] status to PullRequestStatusError failed: %v", pr, err)
|
||||
}
|
||||
return
|
||||
return nil, false
|
||||
}
|
||||
checkAndUpdateStatus(ctx, pr)
|
||||
|
||||
return pr, checkAndUpdateStatus(ctx, pr)
|
||||
}
|
||||
|
||||
// CheckPRsForBaseBranch check all pulls with baseBrannch
|
||||
|
|
|
@ -21,9 +21,9 @@ import (
|
|||
var PRAutoMergeQueue *queue.WorkerPoolQueue[string]
|
||||
|
||||
func addToQueue(pr *issues_model.PullRequest, sha string) {
|
||||
log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha)
|
||||
log.Trace("Adding pullID: %d to the automerge queue with sha %s", pr.ID, sha)
|
||||
if err := PRAutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil {
|
||||
log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err)
|
||||
log.Error("Error adding pullID: %d to the automerge queue %v", pr.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,32 +43,29 @@ func StartPRCheckAndAutoMergeBySHA(ctx context.Context, sha string, repo *repo_m
|
|||
return nil
|
||||
}
|
||||
|
||||
// StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request
|
||||
func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) {
|
||||
if pull == nil || pull.HasMerged || !pull.CanAutoMerge() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := pull.LoadBaseRepo(ctx); err != nil {
|
||||
log.Error("LoadBaseRepo: %v", err)
|
||||
return
|
||||
commitID := pull.HeadCommitID
|
||||
if commitID == "" {
|
||||
commitID = getCommitIDFromRefName(ctx, pull)
|
||||
}
|
||||
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
|
||||
if err != nil {
|
||||
log.Error("OpenRepository: %v", err)
|
||||
return
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
|
||||
if err != nil {
|
||||
log.Error("GetRefCommitID: %v", err)
|
||||
if commitID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
addToQueue(pull, commitID)
|
||||
}
|
||||
|
||||
var AddToQueueIfMergeable = func(ctx context.Context, pull *issues_model.PullRequest) {
|
||||
if pull.Status == issues_model.PullRequestStatusMergeable {
|
||||
StartPRCheckAndAutoMerge(ctx, pull)
|
||||
}
|
||||
}
|
||||
|
||||
func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) {
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
|
@ -118,3 +115,24 @@ func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.
|
|||
|
||||
return pulls, nil
|
||||
}
|
||||
|
||||
func getCommitIDFromRefName(ctx context.Context, pull *issues_model.PullRequest) string {
|
||||
if err := pull.LoadBaseRepo(ctx); err != nil {
|
||||
log.Error("LoadBaseRepo: %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
|
||||
if err != nil {
|
||||
log.Error("OpenRepository: %v", err)
|
||||
return ""
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
|
||||
if err != nil {
|
||||
log.Error("GetRefCommitID: %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return commitID
|
||||
}
|
||||
|
|
345
tests/integration/patch_status_test.go
Normal file
345
tests/integration/patch_status_test.go
Normal file
|
@ -0,0 +1,345 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
issues_model "forgejo.org/models/issues"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
unit_model "forgejo.org/models/unit"
|
||||
"forgejo.org/models/unittest"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/git"
|
||||
"forgejo.org/modules/optional"
|
||||
"forgejo.org/modules/test"
|
||||
pull_service "forgejo.org/services/pull"
|
||||
files_service "forgejo.org/services/repository/files"
|
||||
shared_automerge "forgejo.org/services/shared/automerge"
|
||||
"forgejo.org/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPatchStatus(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
session := loginUser(t, user2.Name)
|
||||
|
||||
repo, _, f := tests.CreateDeclarativeRepoWithOptions(t, user2, tests.DeclarativeRepoOptions{
|
||||
AutoInit: optional.Some(true),
|
||||
EnabledUnits: optional.Some([]unit_model.Type{unit_model.TypeCode}),
|
||||
// commented out for the sake of backporting the test to v11.0
|
||||
// ObjectFormat: optional.Some("sha256"),
|
||||
Files: optional.Some([]*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: ".spokeperson",
|
||||
ContentReader: strings.NewReader("n0toose"),
|
||||
},
|
||||
}),
|
||||
})
|
||||
defer f()
|
||||
|
||||
testAutomergeQueued := func(t *testing.T, pr *issues_model.PullRequest, expected issues_model.PullRequestStatus) {
|
||||
t.Helper()
|
||||
|
||||
var actual issues_model.PullRequestStatus = -1
|
||||
defer test.MockVariableValue(&shared_automerge.AddToQueueIfMergeable, func(ctx context.Context, pull *issues_model.PullRequest) {
|
||||
actual = pull.Status
|
||||
})()
|
||||
|
||||
pull_service.AddToTaskQueue(t.Context(), pr)
|
||||
assert.Eventually(t, func() bool {
|
||||
return expected == actual
|
||||
}, time.Second*5, time.Millisecond*200)
|
||||
}
|
||||
|
||||
testRepoFork(t, session, "user2", repo.Name, "org3", "forked-repo")
|
||||
forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "org3", Name: "forked-repo"})
|
||||
|
||||
u.User = url.UserPassword(user2.Name, userPassword)
|
||||
u.Path = repo.FullName()
|
||||
|
||||
// Clone repository.
|
||||
dstPath := t.TempDir()
|
||||
require.NoError(t, git.Clone(t.Context(), u.String(), dstPath, git.CloneRepoOptions{}))
|
||||
|
||||
// Add `fork` remote.
|
||||
u.Path = forkRepo.FullName()
|
||||
_, _, err := git.NewCommand(git.DefaultContext, "remote", "add", "fork").AddDynamicArguments(u.String()).RunStdString(&git.RunOpts{Dir: dstPath})
|
||||
require.NoError(t, err)
|
||||
|
||||
var normalAGitPR *issues_model.PullRequest
|
||||
|
||||
// Normal pull request, should be mergeable.
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
require.NoError(t, git.NewCommand(t.Context(), "switch", "-c", "normal").AddDynamicArguments(repo.DefaultBranch).Run(&git.RunOpts{Dir: dstPath}))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dstPath, "CONTACT"), []byte("n0toose@example.com"), 0o600))
|
||||
require.NoError(t, git.NewCommand(t.Context(), "add", "CONTACT").Run(&git.RunOpts{Dir: dstPath}))
|
||||
require.NoError(t, git.NewCommand(t.Context(), "commit", "--message=fancy").Run(&git.RunOpts{Dir: dstPath}))
|
||||
|
||||
test := func(t *testing.T, pr *issues_model.PullRequest) {
|
||||
t.Helper()
|
||||
|
||||
assert.Empty(t, pr.ConflictedFiles)
|
||||
assert.Equal(t, issues_model.PullRequestStatusMergeable, pr.Status)
|
||||
assert.Equal(t, 1, pr.CommitsAhead)
|
||||
assert.Equal(t, 0, pr.CommitsBehind)
|
||||
assert.True(t, pr.Mergeable(t.Context()))
|
||||
}
|
||||
|
||||
t.Run("Across repository", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
require.NoError(t, git.NewCommand(t.Context(), "push", "fork", "HEAD:normal").Run(&git.RunOpts{Dir: dstPath}))
|
||||
testPullCreateDirectly(t, session, repo.OwnerName, repo.Name, repo.DefaultBranch, forkRepo.OwnerName, forkRepo.Name, "normal", "across repo normal")
|
||||
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: forkRepo.ID, HeadBranch: "normal"}, "flow = 0")
|
||||
test(t, pr)
|
||||
testAutomergeQueued(t, pr, issues_model.PullRequestStatusMergeable)
|
||||
})
|
||||
|
||||
t.Run("Same repository", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
require.NoError(t, git.NewCommand(t.Context(), "push", "origin", "HEAD:normal").Run(&git.RunOpts{Dir: dstPath}))
|
||||
testPullCreateDirectly(t, session, repo.OwnerName, repo.Name, repo.DefaultBranch, repo.OwnerName, repo.Name, "normal", "same repo normal")
|
||||
|
||||
test(t, unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: repo.ID, HeadBranch: "normal"}, "flow = 0"))
|
||||
})
|
||||
|
||||
t.Run("AGit", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
require.NoError(t, git.NewCommand(t.Context(), "push", "origin", "HEAD:refs/for/main", "-o", "topic=normal").Run(&git.RunOpts{Dir: dstPath}))
|
||||
|
||||
normalAGitPR = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: repo.ID, HeadBranch: "user2/normal", Flow: issues_model.PullRequestFlowAGit})
|
||||
test(t, normalAGitPR)
|
||||
})
|
||||
})
|
||||
|
||||
// If there's a merge conflict, either on update of the base branch or on
|
||||
// creation of the pull request then it should be marked as such.
|
||||
t.Run("Conflict", func(t *testing.T) {
|
||||
require.NoError(t, git.NewCommand(t.Context(), "switch").AddDynamicArguments(repo.DefaultBranch).Run(&git.RunOpts{Dir: dstPath}))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dstPath, "CONTACT"), []byte("gusted@example.com"), 0o600))
|
||||
require.NoError(t, git.NewCommand(t.Context(), "add", "CONTACT").Run(&git.RunOpts{Dir: dstPath}))
|
||||
require.NoError(t, git.NewCommand(t.Context(), "commit", "--message=fancy").Run(&git.RunOpts{Dir: dstPath}))
|
||||
require.NoError(t, git.NewCommand(t.Context(), "push", "origin", "HEAD:main").Run(&git.RunOpts{Dir: dstPath}))
|
||||
require.NoError(t, git.NewCommand(t.Context(), "switch", "normal").Run(&git.RunOpts{Dir: dstPath}))
|
||||
|
||||
// Wait until status check queue is done, we cannot access the queue's
|
||||
// internal information so we rely on the status of the patch being changed.
|
||||
assert.Eventually(t, func() bool {
|
||||
return unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: normalAGitPR.ID}).Status == issues_model.PullRequestStatusConflict
|
||||
}, time.Second*30, time.Millisecond*200)
|
||||
|
||||
test := func(t *testing.T, pr *issues_model.PullRequest) {
|
||||
t.Helper()
|
||||
if assert.Len(t, pr.ConflictedFiles, 1) {
|
||||
assert.Equal(t, "CONTACT", pr.ConflictedFiles[0])
|
||||
}
|
||||
assert.Equal(t, issues_model.PullRequestStatusConflict, pr.Status)
|
||||
assert.Equal(t, 1, pr.CommitsAhead)
|
||||
assert.Equal(t, 1, pr.CommitsBehind)
|
||||
assert.False(t, pr.Mergeable(t.Context()))
|
||||
}
|
||||
|
||||
t.Run("Across repository patch", func(t *testing.T) {
|
||||
t.Run("Existing", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: forkRepo.ID, HeadBranch: "normal"}, "flow = 0")
|
||||
test(t, pr)
|
||||
testAutomergeQueued(t, pr, issues_model.PullRequestStatusConflict)
|
||||
})
|
||||
|
||||
t.Run("New", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
require.NoError(t, git.NewCommand(t.Context(), "push", "fork", "HEAD:conflict").Run(&git.RunOpts{Dir: dstPath}))
|
||||
testPullCreateDirectly(t, session, repo.OwnerName, repo.Name, repo.DefaultBranch, forkRepo.OwnerName, forkRepo.Name, "conflict", "across repo conflict")
|
||||
|
||||
test(t, unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: forkRepo.ID, HeadBranch: "conflict"}, "flow = 0"))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Same repository patch", func(t *testing.T) {
|
||||
t.Run("Existing", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
test(t, unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: repo.ID, HeadBranch: "normal"}, "flow = 0"))
|
||||
})
|
||||
|
||||
t.Run("New", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
require.NoError(t, git.NewCommand(t.Context(), "push", "origin", "HEAD:conflict").Run(&git.RunOpts{Dir: dstPath}))
|
||||
testPullCreateDirectly(t, session, repo.OwnerName, repo.Name, repo.DefaultBranch, repo.OwnerName, repo.Name, "conflict", "same repo conflict")
|
||||
|
||||
test(t, unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: repo.ID, HeadBranch: "conflict"}, "flow = 0"))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("AGit", func(t *testing.T) {
|
||||
t.Run("Existing", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
test(t, unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: repo.ID, HeadBranch: "user2/normal", Flow: issues_model.PullRequestFlowAGit}))
|
||||
})
|
||||
|
||||
t.Run("New", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
require.NoError(t, git.NewCommand(t.Context(), "push", "origin", "HEAD:refs/for/main", "-o", "topic=conflict").Run(&git.RunOpts{Dir: dstPath}))
|
||||
|
||||
test(t, unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: repo.ID, HeadBranch: "user2/conflict", Flow: issues_model.PullRequestFlowAGit}))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Test that the status is set to empty if the diff is empty.
|
||||
t.Run("Empty diff", func(t *testing.T) {
|
||||
require.NoError(t, git.NewCommand(t.Context(), "switch", "-c", "empty-patch").AddDynamicArguments(repo.DefaultBranch).Run(&git.RunOpts{Dir: dstPath}))
|
||||
require.NoError(t, git.NewCommand(t.Context(), "commit", "--allow-empty", "--message=empty").Run(&git.RunOpts{Dir: dstPath}))
|
||||
|
||||
test := func(t *testing.T, pr *issues_model.PullRequest) {
|
||||
t.Helper()
|
||||
|
||||
assert.Empty(t, pr.ConflictedFiles)
|
||||
assert.Equal(t, issues_model.PullRequestStatusEmpty, pr.Status)
|
||||
assert.Equal(t, 1, pr.CommitsAhead)
|
||||
assert.Equal(t, 0, pr.CommitsBehind)
|
||||
assert.True(t, pr.Mergeable(t.Context()))
|
||||
}
|
||||
|
||||
t.Run("Across repository", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
require.NoError(t, git.NewCommand(t.Context(), "push", "fork", "HEAD:empty-patch").Run(&git.RunOpts{Dir: dstPath}))
|
||||
testPullCreateDirectly(t, session, repo.OwnerName, repo.Name, repo.DefaultBranch, forkRepo.OwnerName, forkRepo.Name, "empty-patch", "across repo empty patch")
|
||||
|
||||
test(t, unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: forkRepo.ID, HeadBranch: "empty-patch"}, "flow = 0"))
|
||||
})
|
||||
|
||||
t.Run("Same repository", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
require.NoError(t, git.NewCommand(t.Context(), "push", "origin", "HEAD:empty-patch").Run(&git.RunOpts{Dir: dstPath}))
|
||||
testPullCreateDirectly(t, session, repo.OwnerName, repo.Name, repo.DefaultBranch, repo.OwnerName, repo.Name, "empty-patch", "same repo empty patch")
|
||||
|
||||
test(t, unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: repo.ID, HeadBranch: "empty-patch"}, "flow = 0"))
|
||||
})
|
||||
|
||||
t.Run("AGit", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
require.NoError(t, git.NewCommand(t.Context(), "push", "origin", "HEAD:refs/for/main", "-o", "topic=empty-patch").Run(&git.RunOpts{Dir: dstPath}))
|
||||
|
||||
test(t, unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: repo.ID, HeadBranch: "user2/empty-patch", Flow: issues_model.PullRequestFlowAGit}))
|
||||
})
|
||||
})
|
||||
|
||||
// If a patch modifies a protected file, it should be marked as such.
|
||||
t.Run("Protected file", func(t *testing.T) {
|
||||
// Add protected branch.
|
||||
link := fmt.Sprintf("/%s/settings/branches/edit", repo.FullName())
|
||||
session.MakeRequest(t, NewRequestWithValues(t, "POST", link, map[string]string{
|
||||
"_csrf": GetCSRF(t, session, link),
|
||||
"rule_name": "main",
|
||||
"protected_file_patterns": "LICENSE",
|
||||
}), http.StatusSeeOther)
|
||||
|
||||
require.NoError(t, git.NewCommand(t.Context(), "switch", "-c", "protected").AddDynamicArguments(repo.DefaultBranch).Run(&git.RunOpts{Dir: dstPath}))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dstPath, "LICENSE"), []byte(`# "THE SPEZI-WARE LICENSE" (Revision 2137):
|
||||
|
||||
As long as you retain this notice, you can do whatever you want with this
|
||||
project. If we meet some day, and you think this stuff is worth it, you
|
||||
can buy me/us a Paulaner Spezi in return. ~sdomi, Project SERVFAIL`), 0o600))
|
||||
require.NoError(t, git.NewCommand(t.Context(), "add", "LICENSE").Run(&git.RunOpts{Dir: dstPath}))
|
||||
require.NoError(t, git.NewCommand(t.Context(), "commit", "--message=servfail").Run(&git.RunOpts{Dir: dstPath}))
|
||||
|
||||
test := func(t *testing.T, pr *issues_model.PullRequest) {
|
||||
t.Helper()
|
||||
if assert.Len(t, pr.ChangedProtectedFiles, 1) {
|
||||
assert.Equal(t, "license", pr.ChangedProtectedFiles[0])
|
||||
}
|
||||
assert.Equal(t, issues_model.PullRequestStatusMergeable, pr.Status)
|
||||
assert.Equal(t, 1, pr.CommitsAhead)
|
||||
assert.Equal(t, 0, pr.CommitsBehind)
|
||||
assert.True(t, pr.Mergeable(t.Context()))
|
||||
}
|
||||
|
||||
t.Run("Across repository", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
require.NoError(t, git.NewCommand(t.Context(), "push", "fork", "HEAD:protected").Run(&git.RunOpts{Dir: dstPath}))
|
||||
testPullCreateDirectly(t, session, repo.OwnerName, repo.Name, repo.DefaultBranch, forkRepo.OwnerName, forkRepo.Name, "protected", "accros repo protected")
|
||||
|
||||
test(t, unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: forkRepo.ID, HeadBranch: "protected"}, "flow = 0"))
|
||||
})
|
||||
|
||||
t.Run("Same repository", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
require.NoError(t, git.NewCommand(t.Context(), "push", "origin", "HEAD:protected").Run(&git.RunOpts{Dir: dstPath}))
|
||||
testPullCreateDirectly(t, session, repo.OwnerName, repo.Name, repo.DefaultBranch, repo.OwnerName, repo.Name, "protected", "same repo protected")
|
||||
|
||||
test(t, unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: repo.ID, HeadBranch: "protected"}, "flow = 0"))
|
||||
})
|
||||
|
||||
t.Run("AGit", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
require.NoError(t, git.NewCommand(t.Context(), "push", "origin", "HEAD:refs/for/main", "-o", "topic=protected").Run(&git.RunOpts{Dir: dstPath}))
|
||||
|
||||
test(t, unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: repo.ID, HeadBranch: "user2/protected", Flow: issues_model.PullRequestFlowAGit}))
|
||||
})
|
||||
})
|
||||
|
||||
// If the head branch is a ancestor of the base branch, then it should be marked.
|
||||
t.Run("Ancestor", func(t *testing.T) {
|
||||
require.NoError(t, git.NewCommand(t.Context(), "push", "origin", "protected:protected").Run(&git.RunOpts{Dir: dstPath}))
|
||||
require.NoError(t, git.NewCommand(t.Context(), "switch").AddDynamicArguments(repo.DefaultBranch).Run(&git.RunOpts{Dir: dstPath}))
|
||||
|
||||
test := func(t *testing.T, pr *issues_model.PullRequest) {
|
||||
t.Helper()
|
||||
assert.Equal(t, issues_model.PullRequestStatusAncestor, pr.Status)
|
||||
assert.Equal(t, 0, pr.CommitsAhead)
|
||||
assert.Equal(t, 1, pr.CommitsBehind)
|
||||
assert.True(t, pr.Mergeable(t.Context()))
|
||||
}
|
||||
|
||||
// AGit has a check to not allow AGit to get in this state.
|
||||
|
||||
t.Run("Across repository", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
require.NoError(t, git.NewCommand(t.Context(), "push", "fork", "HEAD:ancestor").Run(&git.RunOpts{Dir: dstPath}))
|
||||
testPullCreateDirectly(t, session, repo.OwnerName, repo.Name, "protected", forkRepo.OwnerName, forkRepo.Name, "ancestor", "accros repo ancestor")
|
||||
|
||||
test(t, unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: forkRepo.ID, HeadBranch: "ancestor"}, "flow = 0"))
|
||||
})
|
||||
|
||||
t.Run("Same repository", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
require.NoError(t, git.NewCommand(t.Context(), "push", "origin", "HEAD:ancestor").Run(&git.RunOpts{Dir: dstPath}))
|
||||
testPullCreateDirectly(t, session, repo.OwnerName, repo.Name, "protected", repo.OwnerName, repo.Name, "ancestor", "same repo ancestor")
|
||||
|
||||
test(t, unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: repo.ID, HeadBranch: "ancestor"}, "flow = 0"))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue