mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-06-09 08:07:40 +00:00
323 lines
15 KiB
Go
323 lines
15 KiB
Go
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||
|
|
||
|
package integration
|
||
|
|
||
|
import (
|
||
|
"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"
|
||
|
files_service "forgejo.org/services/repository/files"
|
||
|
"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}),
|
||
|
ObjectFormat: optional.Some("sha256"),
|
||
|
Files: optional.Some([]*files_service.ChangeRepoFile{
|
||
|
{
|
||
|
Operation: "create",
|
||
|
TreePath: ".spokeperson",
|
||
|
ContentReader: strings.NewReader("n0toose"),
|
||
|
},
|
||
|
}),
|
||
|
})
|
||
|
defer f()
|
||
|
|
||
|
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")
|
||
|
|
||
|
test(t, unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: forkRepo.ID, HeadBranch: "normal"}, "flow = 0"))
|
||
|
})
|
||
|
|
||
|
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)()
|
||
|
|
||
|
test(t, unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: forkRepo.ID, HeadBranch: "normal"}, "flow = 0"))
|
||
|
})
|
||
|
|
||
|
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"))
|
||
|
})
|
||
|
})
|
||
|
})
|
||
|
}
|