mirror of
https://github.com/go-gitea/gitea.git
synced 2025-10-27 05:55:21 +08:00
Merge 0bf39c72e7 into bc50431e8b
This commit is contained in:
commit
fb9782590b
@ -4,7 +4,6 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -339,18 +338,3 @@ func TestGetCommitFileStatusMerges(t *testing.T) {
|
||||
assert.Equal(t, expected.Removed, commitFileStatus.Removed)
|
||||
assert.Equal(t, expected.Modified, commitFileStatus.Modified)
|
||||
}
|
||||
|
||||
func Test_GetCommitBranchStart(t *testing.T) {
|
||||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
|
||||
repo, err := OpenRepository(t.Context(), bareRepo1Path)
|
||||
assert.NoError(t, err)
|
||||
defer repo.Close()
|
||||
commit, err := repo.GetBranchCommit("branch1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "2839944139e0de9737a044f78b0e4b40d989a9e3", commit.ID.String())
|
||||
|
||||
startCommitID, err := repo.GetCommitBranchStart(os.Environ(), "branch1", commit.ID.String())
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, startCommitID)
|
||||
assert.Equal(t, "95bb4d39648ee7e325106df01a621c530863a653", startCommitID)
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@ -288,20 +289,18 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
|
||||
}
|
||||
|
||||
// GetAffectedFiles returns the affected files between two commits
|
||||
func GetAffectedFiles(repo *Repository, branchName, oldCommitID, newCommitID string, env []string) ([]string, error) {
|
||||
if oldCommitID == emptySha1ObjectID.String() || oldCommitID == emptySha256ObjectID.String() {
|
||||
startCommitID, err := repo.GetCommitBranchStart(env, branchName, newCommitID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if startCommitID == "" {
|
||||
return nil, fmt.Errorf("cannot find the start commit of %s", newCommitID)
|
||||
}
|
||||
oldCommitID = startCommitID
|
||||
func GetAffectedFiles(ctx context.Context, repoPath, oldCommitID, newCommitID string, env []string) ([]string, error) {
|
||||
if oldCommitID == emptySha1ObjectID.String() {
|
||||
oldCommitID = emptySha1ObjectID.Type().EmptyTree().String()
|
||||
} else if oldCommitID == emptySha256ObjectID.String() {
|
||||
oldCommitID = emptySha256ObjectID.Type().EmptyTree().String()
|
||||
} else if oldCommitID == "" {
|
||||
return nil, errors.New("oldCommitID is empty")
|
||||
}
|
||||
|
||||
stdoutReader, stdoutWriter, err := os.Pipe()
|
||||
if err != nil {
|
||||
log.Error("Unable to create os.Pipe for %s", repo.Path)
|
||||
log.Error("Unable to create os.Pipe for %s", repoPath)
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
@ -314,7 +313,7 @@ func GetAffectedFiles(repo *Repository, branchName, oldCommitID, newCommitID str
|
||||
// Run `git diff --name-only` to get the names of the changed files
|
||||
err = gitcmd.NewCommand("diff", "--name-only").AddDynamicArguments(oldCommitID, newCommitID).
|
||||
WithEnv(env).
|
||||
WithDir(repo.Path).
|
||||
WithDir(repoPath).
|
||||
WithStdout(stdoutWriter).
|
||||
WithPipelineFunc(func(ctx context.Context, cancel context.CancelFunc) error {
|
||||
// Close the writer end of the pipe to begin processing
|
||||
@ -334,9 +333,9 @@ func GetAffectedFiles(repo *Repository, branchName, oldCommitID, newCommitID str
|
||||
}
|
||||
return scanner.Err()
|
||||
}).
|
||||
Run(repo.Ctx)
|
||||
Run(ctx)
|
||||
if err != nil {
|
||||
log.Error("Unable to get affected files for commits from %s to %s in %s: %v", oldCommitID, newCommitID, repo.Path, err)
|
||||
log.Error("Unable to get affected files for commits from %s to %s in %s: %v", oldCommitID, newCommitID, repoPath, err)
|
||||
}
|
||||
|
||||
return affectedFiles, err
|
||||
|
||||
@ -31,6 +31,7 @@ type Features struct {
|
||||
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
|
||||
SupportedObjectFormats []ObjectFormat // sha1, sha256
|
||||
SupportCheckAttrOnBare bool // >= 2.40
|
||||
SupportGitMergeTree bool // >= 2.38
|
||||
}
|
||||
|
||||
var defaultFeatures *Features
|
||||
@ -75,6 +76,7 @@ func loadGitVersionFeatures() (*Features, error) {
|
||||
features.SupportedObjectFormats = append(features.SupportedObjectFormats, Sha256ObjectFormat)
|
||||
}
|
||||
features.SupportCheckAttrOnBare = features.CheckVersionAtLeast("2.40")
|
||||
features.SupportGitMergeTree = features.CheckVersionAtLeast("2.38")
|
||||
return features, nil
|
||||
}
|
||||
|
||||
|
||||
@ -456,6 +456,17 @@ func IsErrorExitCode(err error, code int) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func ExitCode(err error) (int, bool) {
|
||||
if err == nil {
|
||||
return 0, true
|
||||
}
|
||||
var exitError *exec.ExitError
|
||||
if errors.As(err, &exitError) {
|
||||
return exitError.ExitCode(), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// RunStdString runs the command and returns stdout/stderr as string. and store stderr to returned error (err combined with stderr).
|
||||
func (c *Command) RunStdString(ctx context.Context) (stdout, stderr string, runErr RunStdError) {
|
||||
stdoutBytes, stderrBytes, runErr := c.WithParentCallerInfo().runStdBytes(ctx)
|
||||
|
||||
@ -580,34 +580,3 @@ func (repo *Repository) AddLastCommitCache(cacheKey, fullName, sha string) error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCommitBranchStart returns the commit where the branch diverged
|
||||
func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID string) (string, error) {
|
||||
cmd := gitcmd.NewCommand("log", prettyLogFormat)
|
||||
cmd.AddDynamicArguments(endCommitID)
|
||||
|
||||
stdout, _, runErr := cmd.WithDir(repo.Path).
|
||||
WithEnv(env).
|
||||
RunStdBytes(repo.Ctx)
|
||||
if runErr != nil {
|
||||
return "", runErr
|
||||
}
|
||||
|
||||
parts := bytes.SplitSeq(bytes.TrimSpace(stdout), []byte{'\n'})
|
||||
|
||||
// check the commits one by one until we find a commit contained by another branch
|
||||
// and we think this commit is the divergence point
|
||||
for commitID := range parts {
|
||||
branches, err := repo.getBranches(env, string(commitID), 2)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, b := range branches {
|
||||
if b != branch {
|
||||
return string(commitID), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
16
modules/gitrepo/fetch.go
Normal file
16
modules/gitrepo/fetch.go
Normal file
@ -0,0 +1,16 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package gitrepo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
)
|
||||
|
||||
func FetchRemoteCommit(ctx context.Context, repo, remoteRepo Repository, commitID string) error {
|
||||
return RunCmd(ctx, repo, gitcmd.NewCommand("fetch", "--no-tags").
|
||||
AddDynamicArguments(repoPath(remoteRepo)).
|
||||
AddDynamicArguments(commitID))
|
||||
}
|
||||
75
modules/gitrepo/merge.go
Normal file
75
modules/gitrepo/merge.go
Normal file
@ -0,0 +1,75 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package gitrepo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
)
|
||||
|
||||
func MergeBase(ctx context.Context, repo Repository, commit1, commit2 string) (string, error) {
|
||||
mergeBase, err := RunCmdString(ctx, repo, gitcmd.NewCommand("merge-base", "--").
|
||||
AddDynamicArguments(commit1, commit2))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get merge-base of %s and %s failed: %w", commit1, commit2, err)
|
||||
}
|
||||
return strings.TrimSpace(mergeBase), nil
|
||||
}
|
||||
|
||||
// parseMergeTreeOutput parses the output of git merge-tree --write-tree -z --name-only --no-messages
|
||||
// For a successful merge, the output is a simply one line <OID of toplevel tree>NUL
|
||||
// Whereas for a conflicted merge, the output is:
|
||||
// <OID of toplevel tree>NUL
|
||||
// <Conflicted file name 1>NUL
|
||||
// <Conflicted file name 2>NUL
|
||||
// ...
|
||||
// ref: https://git-scm.com/docs/git-merge-tree/2.38.0#OUTPUT
|
||||
func parseMergeTreeOutput(output string) (string, []string, error) {
|
||||
fields := strings.Split(strings.TrimSuffix(output, "\x00"), "\x00")
|
||||
switch len(fields) {
|
||||
case 0:
|
||||
return "", nil, errors.New("unexpected empty output")
|
||||
case 1:
|
||||
return strings.TrimSpace(fields[0]), nil, nil
|
||||
default:
|
||||
return strings.TrimSpace(fields[0]), fields[1:], nil
|
||||
}
|
||||
}
|
||||
|
||||
// MergeTree performs a merge between two commits (baseRef and headRef) with an optional merge base.
|
||||
// It returns the resulting tree hash, a list of conflicted files (if any), and an error if the operation fails.
|
||||
// If there are no conflicts, the list of conflicted files will be nil.
|
||||
func MergeTree(ctx context.Context, repo Repository, baseRef, headRef, mergeBase string) (string, bool, []string, error) {
|
||||
cmd := gitcmd.NewCommand("merge-tree", "--write-tree", "-z", "--name-only", "--no-messages")
|
||||
if git.DefaultFeatures().CheckVersionAtLeast("2.40") && mergeBase != "" {
|
||||
cmd.AddOptionFormat("--merge-base=%s", mergeBase)
|
||||
}
|
||||
|
||||
stdout := &bytes.Buffer{}
|
||||
gitErr := RunCmd(ctx, repo, cmd.AddDynamicArguments(baseRef, headRef).WithStdout(stdout))
|
||||
exitCode, ok := gitcmd.ExitCode(gitErr)
|
||||
if !ok {
|
||||
return "", false, nil, fmt.Errorf("run merge-tree failed: %w", gitErr)
|
||||
}
|
||||
|
||||
switch exitCode {
|
||||
case 0, 1:
|
||||
treeID, conflictedFiles, err := parseMergeTreeOutput(stdout.String())
|
||||
if err != nil {
|
||||
return "", false, nil, fmt.Errorf("parse merge-tree output failed: %w", err)
|
||||
}
|
||||
// For a successful, non-conflicted merge, the exit status is 0. When the merge has conflicts, the exit status is 1.
|
||||
// A merge can have conflicts without having individual files conflict
|
||||
// https://git-scm.com/docs/git-merge-tree/2.38.0#_mistakes_to_avoid
|
||||
return treeID, exitCode == 1, conflictedFiles, nil
|
||||
default:
|
||||
return "", false, nil, fmt.Errorf("run merge-tree exit abnormally: %w", gitErr)
|
||||
}
|
||||
}
|
||||
26
modules/gitrepo/merge_test.go
Normal file
26
modules/gitrepo/merge_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package gitrepo
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_parseMergeTreeOutput(t *testing.T) {
|
||||
conflictedOutput := "837480c2773160381cbe6bcce90f7732789b5856\x00options/locale/locale_en-US.ini\x00services/webhook/webhook_test.go\x00"
|
||||
treeID, conflictedFiles, err := parseMergeTreeOutput(conflictedOutput)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "837480c2773160381cbe6bcce90f7732789b5856", treeID)
|
||||
assert.Len(t, conflictedFiles, 2)
|
||||
assert.Equal(t, "options/locale/locale_en-US.ini", conflictedFiles[0])
|
||||
assert.Equal(t, "services/webhook/webhook_test.go", conflictedFiles[1])
|
||||
|
||||
nonConflictedOutput := "837480c2773160381cbe6bcce90f7732789b5856\x00"
|
||||
treeID, conflictedFiles, err = parseMergeTreeOutput(nonConflictedOutput)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "837480c2773160381cbe6bcce90f7732789b5856", treeID)
|
||||
assert.Empty(t, conflictedFiles)
|
||||
}
|
||||
@ -244,7 +244,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
|
||||
|
||||
globs := protectBranch.GetProtectedFilePatterns()
|
||||
if len(globs) > 0 {
|
||||
_, err := pull_service.CheckFileProtection(gitRepo, branchName, oldCommitID, newCommitID, globs, 1, ctx.env)
|
||||
_, err := pull_service.CheckFileProtection(ctx, repo.RepoPath(), oldCommitID, newCommitID, globs, 1, ctx.env)
|
||||
if err != nil {
|
||||
if !pull_service.IsErrFilePathProtected(err) {
|
||||
log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
|
||||
@ -302,7 +302,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
|
||||
// Allow commits that only touch unprotected files
|
||||
globs := protectBranch.GetUnprotectedFilePatterns()
|
||||
if len(globs) > 0 {
|
||||
unprotectedFilesOnly, err := pull_service.CheckUnprotectedFiles(gitRepo, branchName, oldCommitID, newCommitID, globs, ctx.env)
|
||||
unprotectedFilesOnly, err := pull_service.CheckUnprotectedFiles(ctx, repo, oldCommitID, newCommitID, globs, ctx.env)
|
||||
if err != nil {
|
||||
log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
|
||||
@ -437,7 +437,7 @@ func checkPullRequestMergeable(id int64) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := testPullRequestBranchMergeable(pr); err != nil {
|
||||
if err := checkPullRequestMergeableAndUpdateStatus(ctx, pr); err != nil {
|
||||
log.Error("testPullRequestTmpRepoBranchMergeable[%-v]: %v", pr, err)
|
||||
pr.Status = issues_model.PullRequestStatusError
|
||||
if err := pr.UpdateCols(ctx, "status"); err != nil {
|
||||
|
||||
155
services/pull/conflicts.go
Normal file
155
services/pull/conflicts.go
Normal file
@ -0,0 +1,155 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pull
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// checkPullRequestMergeableAndUpdateStatus checks whether a pull request is mergeable and updates its status accordingly.
|
||||
// It uses 'git merge-tree' if supported by the Git version, otherwise it falls back to using a temporary repository.
|
||||
// This function updates the pr.Status, pr.MergeBase and pr.ConflictedFiles fields as necessary.
|
||||
// The pull request parameter may not be created yet in the database, so do not assume it has an ID.
|
||||
func checkPullRequestMergeableAndUpdateStatus(ctx context.Context, pr *issues_model.PullRequest) error {
|
||||
if git.DefaultFeatures().SupportGitMergeTree {
|
||||
return checkPullRequestMergeableAndUpdateStatusMergeTree(ctx, pr)
|
||||
}
|
||||
|
||||
return checkPullRequestMergeableAndUpdateStatusTmpRepo(ctx, pr)
|
||||
}
|
||||
|
||||
// checkConflictsMergeTree uses git merge-tree to check for conflicts and if none are found checks if the patch is empty
|
||||
// return true if there is conflicts otherwise return false
|
||||
// pr.Status and pr.ConflictedFiles will be updated as necessary
|
||||
func checkConflictsMergeTree(ctx context.Context, pr *issues_model.PullRequest, baseCommitID string) (bool, error) {
|
||||
treeHash, conflict, conflictFiles, err := gitrepo.MergeTree(ctx, pr.BaseRepo, baseCommitID, pr.HeadCommitID, pr.MergeBase)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("MergeTree: %w", err)
|
||||
}
|
||||
if conflict {
|
||||
pr.Status = issues_model.PullRequestStatusConflict
|
||||
pr.ConflictedFiles = util.Iif(len(conflictFiles) > 0, conflictFiles, []string{"(no files listed)"})
|
||||
|
||||
log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Detecting whether the pull request has difference via git diff-tree
|
||||
// it will return exit code 0 if there's no diff and exit code 1 if there's a diff.
|
||||
gitErr := gitrepo.RunCmd(ctx, pr.BaseRepo, gitcmd.NewCommand("diff-tree", "-r", "--quiet").
|
||||
AddDynamicArguments(treeHash, pr.MergeBase))
|
||||
exitCode, ok := gitcmd.ExitCode(gitErr)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("run diff-tree failed: %w", gitErr)
|
||||
}
|
||||
|
||||
switch exitCode {
|
||||
case 0:
|
||||
log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID)
|
||||
pr.Status = issues_model.PullRequestStatusEmpty
|
||||
case 1:
|
||||
pr.Status = issues_model.PullRequestStatusMergeable
|
||||
default:
|
||||
return false, fmt.Errorf("run diff-tree exit abnormally: %w", gitErr)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func checkPullRequestMergeableAndUpdateStatusMergeTree(ctx context.Context, pr *issues_model.PullRequest) error {
|
||||
// 1. Get head commit
|
||||
if err := pr.LoadHeadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
headGitRepo, err := gitrepo.OpenRepository(ctx, pr.HeadRepo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("OpenRepository: %w", err)
|
||||
}
|
||||
defer headGitRepo.Close()
|
||||
|
||||
// 2. Get base commit id
|
||||
var baseGitRepo *git.Repository
|
||||
if pr.IsSameRepo() {
|
||||
baseGitRepo = headGitRepo
|
||||
} else {
|
||||
baseGitRepo, err = gitrepo.OpenRepository(ctx, pr.BaseRepo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("OpenRepository: %w", err)
|
||||
}
|
||||
defer baseGitRepo.Close()
|
||||
}
|
||||
|
||||
// 3. Get head commit id
|
||||
if pr.Flow == issues_model.PullRequestFlowGithub {
|
||||
pr.HeadCommitID, err = headGitRepo.GetRefCommitID(git.BranchPrefix + pr.HeadBranch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetBranchCommitID: can't find commit ID for head: %w", err)
|
||||
}
|
||||
} else {
|
||||
if pr.ID > 0 {
|
||||
pr.HeadCommitID, err = baseGitRepo.GetRefCommitID(pr.GetGitHeadRefName())
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetRefCommitID: can't find commit ID for head: %w", err)
|
||||
}
|
||||
} else if pr.HeadCommitID == "" { // for new pull request with agit, the head commit id must be provided
|
||||
return errors.New("head commit ID is empty for pull request Agit flow")
|
||||
}
|
||||
}
|
||||
|
||||
// 4. fetch head commit id into the current repository
|
||||
// it will be checked in 2 weeks by default from git if the pull request created failure.
|
||||
if !pr.IsSameRepo() {
|
||||
if err := gitrepo.FetchRemoteCommit(ctx, pr.BaseRepo, pr.HeadRepo, pr.HeadCommitID); err != nil {
|
||||
return fmt.Errorf("FetchRemoteCommit: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. update merge base
|
||||
baseCommitID, err := baseGitRepo.GetRefCommitID(git.BranchPrefix + pr.BaseBranch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetBranchCommitID: can't find commit ID for base: %w", err)
|
||||
}
|
||||
|
||||
pr.MergeBase, err = gitrepo.MergeBase(ctx, pr.BaseRepo, baseCommitID, pr.HeadCommitID)
|
||||
if err != nil {
|
||||
log.Error("GetMergeBase: %v and can't find commit ID for base: %v", err, baseCommitID)
|
||||
pr.Status = issues_model.PullRequestStatusEmpty // if there is no merge base, then it's empty but we still need to allow the pull request created
|
||||
return nil
|
||||
}
|
||||
|
||||
// 6. if base == head, then it's an ancestor
|
||||
if pr.HeadCommitID == pr.MergeBase {
|
||||
pr.Status = issues_model.PullRequestStatusAncestor
|
||||
return nil
|
||||
}
|
||||
|
||||
// 7. Check for conflicts
|
||||
conflicted, err := checkConflictsMergeTree(ctx, pr, baseCommitID)
|
||||
if err != nil {
|
||||
log.Error("checkConflictsMergeTree: %v", err)
|
||||
pr.Status = issues_model.PullRequestStatusEmpty // if there is no merge base, then it's empty but we still need to allow the pull request created
|
||||
}
|
||||
if conflicted || pr.Status == issues_model.PullRequestStatusEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 7. Check for protected files changes
|
||||
if err = checkPullFilesProtection(ctx, pr, pr.BaseRepo.RepoPath()); err != nil {
|
||||
return fmt.Errorf("pr.CheckPullFilesProtection(): %v", err)
|
||||
}
|
||||
if len(pr.ChangedProtectedFiles) > 0 {
|
||||
log.Trace("Found %d protected files changed", len(pr.ChangedProtectedFiles))
|
||||
}
|
||||
|
||||
pr.Status = issues_model.PullRequestStatusMergeable
|
||||
return nil
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
// Copyright 2019 The Gitea Authors.
|
||||
// All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pull
|
||||
@ -15,15 +14,14 @@ import (
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/glob"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
@ -67,10 +65,7 @@ var patchErrorSuffices = []string{
|
||||
": does not exist in index",
|
||||
}
|
||||
|
||||
func testPullRequestBranchMergeable(pr *issues_model.PullRequest) error {
|
||||
ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("testPullRequestBranchMergeable: %s", pr))
|
||||
defer finished()
|
||||
|
||||
func checkPullRequestMergeableAndUpdateStatusTmpRepo(ctx context.Context, pr *issues_model.PullRequest) error {
|
||||
prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
|
||||
if err != nil {
|
||||
if !git_model.IsErrBranchNotExist(err) {
|
||||
@ -80,10 +75,6 @@ func testPullRequestBranchMergeable(pr *issues_model.PullRequest) error {
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
return testPullRequestTmpRepoBranchMergeable(ctx, prCtx, pr)
|
||||
}
|
||||
|
||||
func testPullRequestTmpRepoBranchMergeable(ctx context.Context, prCtx *prTmpRepoContext, pr *issues_model.PullRequest) error {
|
||||
gitRepo, err := git.OpenRepository(ctx, prCtx.tmpBasePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("OpenRepository: %w", err)
|
||||
@ -115,7 +106,7 @@ func testPullRequestTmpRepoBranchMergeable(ctx context.Context, prCtx *prTmpRepo
|
||||
}
|
||||
|
||||
// 3. Check for protected files changes
|
||||
if err = checkPullFilesProtection(ctx, pr, gitRepo); err != nil {
|
||||
if err = checkPullFilesProtection(ctx, pr, prCtx.tmpBasePath); err != nil {
|
||||
return fmt.Errorf("pr.CheckPullFilesProtection(): %v", err)
|
||||
}
|
||||
|
||||
@ -530,11 +521,11 @@ func (err ErrFilePathProtected) Unwrap() error {
|
||||
}
|
||||
|
||||
// CheckFileProtection check file Protection
|
||||
func CheckFileProtection(repo *git.Repository, branchName, oldCommitID, newCommitID string, patterns []glob.Glob, limit int, env []string) ([]string, error) {
|
||||
func CheckFileProtection(ctx context.Context, repoPath, oldCommitID, newCommitID string, patterns []glob.Glob, limit int, env []string) ([]string, error) {
|
||||
if len(patterns) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
affectedFiles, err := git.GetAffectedFiles(repo, branchName, oldCommitID, newCommitID, env)
|
||||
affectedFiles, err := git.GetAffectedFiles(ctx, repoPath, oldCommitID, newCommitID, env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -560,11 +551,11 @@ func CheckFileProtection(repo *git.Repository, branchName, oldCommitID, newCommi
|
||||
}
|
||||
|
||||
// CheckUnprotectedFiles check if the commit only touches unprotected files
|
||||
func CheckUnprotectedFiles(repo *git.Repository, branchName, oldCommitID, newCommitID string, patterns []glob.Glob, env []string) (bool, error) {
|
||||
func CheckUnprotectedFiles(ctx context.Context, repo *repo_model.Repository, oldCommitID, newCommitID string, patterns []glob.Glob, env []string) (bool, error) {
|
||||
if len(patterns) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
affectedFiles, err := git.GetAffectedFiles(repo, branchName, oldCommitID, newCommitID, env)
|
||||
affectedFiles, err := git.GetAffectedFiles(ctx, repo.RepoPath(), oldCommitID, newCommitID, env)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -585,7 +576,8 @@ func CheckUnprotectedFiles(repo *git.Repository, branchName, oldCommitID, newCom
|
||||
}
|
||||
|
||||
// checkPullFilesProtection check if pr changed protected files and save results
|
||||
func checkPullFilesProtection(ctx context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) error {
|
||||
// repoPath might be a temporary path so that we need to pass it in
|
||||
func checkPullFilesProtection(ctx context.Context, pr *issues_model.PullRequest, repoPath string) error {
|
||||
if pr.Status == issues_model.PullRequestStatusEmpty {
|
||||
pr.ChangedProtectedFiles = nil
|
||||
return nil
|
||||
@ -601,7 +593,7 @@ func checkPullFilesProtection(ctx context.Context, pr *issues_model.PullRequest,
|
||||
return nil
|
||||
}
|
||||
|
||||
pr.ChangedProtectedFiles, err = CheckFileProtection(gitRepo, pr.HeadBranch, pr.MergeBase, "tracking", pb.GetProtectedFilePatterns(), 10, os.Environ())
|
||||
pr.ChangedProtectedFiles, err = CheckFileProtection(ctx, repoPath, pr.MergeBase, pr.HeadCommitID, pb.GetProtectedFilePatterns(), 10, os.Environ())
|
||||
if err != nil && !IsErrFilePathProtected(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -30,6 +30,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/globallock"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@ -86,16 +87,12 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
|
||||
}
|
||||
}
|
||||
|
||||
prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
|
||||
if err != nil {
|
||||
if !git_model.IsErrBranchNotExist(err) {
|
||||
log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("testPullRequestBranchMergeable: %s", pr))
|
||||
defer finished()
|
||||
|
||||
if err := testPullRequestTmpRepoBranchMergeable(ctx, prCtx, pr); err != nil {
|
||||
// the pull request haven't been created
|
||||
err := checkPullRequestMergeableAndUpdateStatus(ctx, pr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -290,8 +287,11 @@ func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer
|
||||
oldBranch := pr.BaseBranch
|
||||
pr.BaseBranch = targetBranch
|
||||
|
||||
ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("testPullRequestBranchMergeable: %s", pr))
|
||||
defer finished()
|
||||
|
||||
// Refresh patch
|
||||
if err := testPullRequestBranchMergeable(pr); err != nil {
|
||||
if err := checkPullRequestMergeableAndUpdateStatus(ctx, pr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,8 @@ import (
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
issues_service "code.gitea.io/gitea/services/issue"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -96,14 +98,40 @@ func testPullCommentRetarget(t *testing.T, u *url.URL, session *TestSession) {
|
||||
testWaitForPullRequestStatus(t, &issues_model.Issue{Title: testPRTitle}, issues_model.PullRequestStatusMergeable)
|
||||
}
|
||||
|
||||
func TestPullComment(t *testing.T) {
|
||||
func TestPullComment_MergeTree(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
session := loginUser(t, "user1")
|
||||
testCreateBranch(t, session, "user2", "repo1", "branch/master", "test-branch/rebase", http.StatusSeeOther)
|
||||
testCreateBranch(t, session, "user2", "repo1", "branch/master", "test-branch/retarget", http.StatusSeeOther)
|
||||
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
|
||||
|
||||
t.Run("RebaseComment", func(t *testing.T) { testPullCommentRebase(t, u, session) })
|
||||
t.Run("RetargetComment", func(t *testing.T) { testPullCommentRetarget(t, u, session) })
|
||||
t.Run("RebaseComment_MergeTree", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, true)()
|
||||
testPullCommentRebase(t, u, session)
|
||||
})
|
||||
|
||||
t.Run("RetargetComment_MergeTree", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, true)()
|
||||
testPullCommentRetarget(t, u, session)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestPullComment_TmpRepo(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
session := loginUser(t, "user1")
|
||||
testCreateBranch(t, session, "user2", "repo1", "branch/master", "test-branch/rebase", http.StatusSeeOther)
|
||||
testCreateBranch(t, session, "user2", "repo1", "branch/master", "test-branch/retarget", http.StatusSeeOther)
|
||||
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
|
||||
|
||||
t.Run("RebaseComment_TmpRepo", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, false)()
|
||||
testPullCommentRebase(t, u, session)
|
||||
})
|
||||
|
||||
t.Run("RetargetComment_TmpRepo", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, false)()
|
||||
testPullCommentRetarget(t, u, session)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -4,9 +4,18 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/commitstatus"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
@ -39,3 +48,98 @@ func TestListPullCommits(t *testing.T) {
|
||||
assert.Contains(t, resp.Body.String(), `<td class="lines-code lines-code-new"><code class="code-inner"># repo1</code>`)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPullCreate_CommitStatus(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
session := loginUser(t, "user1")
|
||||
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
|
||||
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1")
|
||||
|
||||
url := path.Join("user1", "repo1", "compare", "master...status1")
|
||||
req := NewRequestWithValues(t, "POST", url,
|
||||
map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
"title": "pull request from status1",
|
||||
},
|
||||
)
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequest(t, "GET", "/user1/repo1/pulls")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
NewHTMLParser(t, resp.Body)
|
||||
|
||||
// Request repository commits page
|
||||
req = NewRequest(t, "GET", "/user1/repo1/pulls/1/commits")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
// Get first commit URL
|
||||
commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href")
|
||||
assert.True(t, exists)
|
||||
assert.NotEmpty(t, commitURL)
|
||||
|
||||
commitID := path.Base(commitURL)
|
||||
|
||||
statusList := []commitstatus.CommitStatusState{
|
||||
commitstatus.CommitStatusPending,
|
||||
commitstatus.CommitStatusError,
|
||||
commitstatus.CommitStatusFailure,
|
||||
commitstatus.CommitStatusSuccess,
|
||||
commitstatus.CommitStatusWarning,
|
||||
}
|
||||
|
||||
statesIcons := map[commitstatus.CommitStatusState]string{
|
||||
commitstatus.CommitStatusPending: "octicon-dot-fill",
|
||||
commitstatus.CommitStatusSuccess: "octicon-check",
|
||||
commitstatus.CommitStatusError: "gitea-exclamation",
|
||||
commitstatus.CommitStatusFailure: "octicon-x",
|
||||
commitstatus.CommitStatusWarning: "gitea-exclamation",
|
||||
}
|
||||
|
||||
testCtx := NewAPITestContext(t, "user1", "repo1", auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
// Update commit status, and check if icon is updated as well
|
||||
for _, status := range statusList {
|
||||
// Call API to add status for commit
|
||||
t.Run("CreateStatus", doAPICreateCommitStatus(testCtx, commitID, api.CreateStatusOption{
|
||||
State: status,
|
||||
TargetURL: "http://test.ci/",
|
||||
Description: "",
|
||||
Context: "testci",
|
||||
}))
|
||||
|
||||
req = NewRequest(t, "GET", "/user1/repo1/pulls/1/commits")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
doc = NewHTMLParser(t, resp.Body)
|
||||
|
||||
commitURL, exists = doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href")
|
||||
assert.True(t, exists)
|
||||
assert.NotEmpty(t, commitURL)
|
||||
assert.Equal(t, commitID, path.Base(commitURL))
|
||||
|
||||
cls, ok := doc.doc.Find("#commits-table tbody tr td.message .commit-status").Last().Attr("class")
|
||||
assert.True(t, ok)
|
||||
assert.Contains(t, cls, statesIcons[status])
|
||||
}
|
||||
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"})
|
||||
css := unittest.AssertExistsAndLoadBean(t, &git_model.CommitStatusSummary{RepoID: repo1.ID, SHA: commitID})
|
||||
assert.Equal(t, commitstatus.CommitStatusSuccess, css.State)
|
||||
})
|
||||
}
|
||||
|
||||
func doAPICreateCommitStatus(ctx APITestContext, commitID string, data api.CreateStatusOption) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
req := NewRequestWithJSON(
|
||||
t,
|
||||
http.MethodPost,
|
||||
fmt.Sprintf("/api/v1/repos/%s/%s/statuses/%s", ctx.Username, ctx.Reponame, commitID),
|
||||
data,
|
||||
).AddTokenAuth(ctx.Token)
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
ctx.Session.MakeRequest(t, req, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,9 +36,7 @@ import (
|
||||
"code.gitea.io/gitea/services/automerge"
|
||||
"code.gitea.io/gitea/services/automergequeue"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@ -523,86 +521,6 @@ func TestCantFastForwardOnlyMergeDiverging(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestConflictChecking(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
// Create new clean repo to test conflict checking.
|
||||
baseRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{
|
||||
Name: "conflict-checking",
|
||||
Description: "Tempo repo",
|
||||
AutoInit: true,
|
||||
Readme: "Default",
|
||||
DefaultBranch: "main",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, baseRepo)
|
||||
|
||||
// create a commit on new branch.
|
||||
_, err = files_service.ChangeRepoFiles(t.Context(), baseRepo, user, &files_service.ChangeRepoFilesOptions{
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: "important_file",
|
||||
ContentReader: strings.NewReader("Just a non-important file"),
|
||||
},
|
||||
},
|
||||
Message: "Add a important file",
|
||||
OldBranch: "main",
|
||||
NewBranch: "important-secrets",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// create a commit on main branch.
|
||||
_, err = files_service.ChangeRepoFiles(t.Context(), baseRepo, user, &files_service.ChangeRepoFilesOptions{
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: "important_file",
|
||||
ContentReader: strings.NewReader("Not the same content :P"),
|
||||
},
|
||||
},
|
||||
Message: "Add a important file",
|
||||
OldBranch: "main",
|
||||
NewBranch: "main",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// create Pull to merge the important-secrets branch into main branch.
|
||||
pullIssue := &issues_model.Issue{
|
||||
RepoID: baseRepo.ID,
|
||||
Title: "PR with conflict!",
|
||||
PosterID: user.ID,
|
||||
Poster: user,
|
||||
IsPull: true,
|
||||
}
|
||||
|
||||
pullRequest := &issues_model.PullRequest{
|
||||
HeadRepoID: baseRepo.ID,
|
||||
BaseRepoID: baseRepo.ID,
|
||||
HeadBranch: "important-secrets",
|
||||
BaseBranch: "main",
|
||||
HeadRepo: baseRepo,
|
||||
BaseRepo: baseRepo,
|
||||
Type: issues_model.PullRequestGitea,
|
||||
}
|
||||
prOpts := &pull_service.NewPullRequestOptions{Repo: baseRepo, Issue: pullIssue, PullRequest: pullRequest}
|
||||
err = pull_service.NewPullRequest(t.Context(), prOpts)
|
||||
assert.NoError(t, err)
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"})
|
||||
assert.NoError(t, issue.LoadPullRequest(t.Context()))
|
||||
conflictingPR := issue.PullRequest
|
||||
|
||||
// Ensure conflictedFiles is populated.
|
||||
assert.Len(t, conflictingPR.ConflictedFiles, 1)
|
||||
// Check if status is correct.
|
||||
assert.Equal(t, issues_model.PullRequestStatusConflict, conflictingPR.Status)
|
||||
// Ensure that mergeable returns false
|
||||
assert.False(t, conflictingPR.Mergeable(t.Context()))
|
||||
})
|
||||
}
|
||||
|
||||
func TestPullRetargetChildOnBranchDelete(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
session := loginUser(t, "user1")
|
||||
|
||||
@ -11,115 +11,20 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
"code.gitea.io/gitea/models/issues"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/commitstatus"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/services/pull"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPullCreate_CommitStatus(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
session := loginUser(t, "user1")
|
||||
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
|
||||
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1")
|
||||
|
||||
url := path.Join("user1", "repo1", "compare", "master...status1")
|
||||
req := NewRequestWithValues(t, "POST", url,
|
||||
map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
"title": "pull request from status1",
|
||||
},
|
||||
)
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequest(t, "GET", "/user1/repo1/pulls")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
NewHTMLParser(t, resp.Body)
|
||||
|
||||
// Request repository commits page
|
||||
req = NewRequest(t, "GET", "/user1/repo1/pulls/1/commits")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
// Get first commit URL
|
||||
commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href")
|
||||
assert.True(t, exists)
|
||||
assert.NotEmpty(t, commitURL)
|
||||
|
||||
commitID := path.Base(commitURL)
|
||||
|
||||
statusList := []commitstatus.CommitStatusState{
|
||||
commitstatus.CommitStatusPending,
|
||||
commitstatus.CommitStatusError,
|
||||
commitstatus.CommitStatusFailure,
|
||||
commitstatus.CommitStatusSuccess,
|
||||
commitstatus.CommitStatusWarning,
|
||||
}
|
||||
|
||||
statesIcons := map[commitstatus.CommitStatusState]string{
|
||||
commitstatus.CommitStatusPending: "octicon-dot-fill",
|
||||
commitstatus.CommitStatusSuccess: "octicon-check",
|
||||
commitstatus.CommitStatusError: "gitea-exclamation",
|
||||
commitstatus.CommitStatusFailure: "octicon-x",
|
||||
commitstatus.CommitStatusWarning: "gitea-exclamation",
|
||||
}
|
||||
|
||||
testCtx := NewAPITestContext(t, "user1", "repo1", auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
// Update commit status, and check if icon is updated as well
|
||||
for _, status := range statusList {
|
||||
// Call API to add status for commit
|
||||
t.Run("CreateStatus", doAPICreateCommitStatus(testCtx, commitID, api.CreateStatusOption{
|
||||
State: status,
|
||||
TargetURL: "http://test.ci/",
|
||||
Description: "",
|
||||
Context: "testci",
|
||||
}))
|
||||
|
||||
req = NewRequest(t, "GET", "/user1/repo1/pulls/1/commits")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
doc = NewHTMLParser(t, resp.Body)
|
||||
|
||||
commitURL, exists = doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href")
|
||||
assert.True(t, exists)
|
||||
assert.NotEmpty(t, commitURL)
|
||||
assert.Equal(t, commitID, path.Base(commitURL))
|
||||
|
||||
cls, ok := doc.doc.Find("#commits-table tbody tr td.message .commit-status").Last().Attr("class")
|
||||
assert.True(t, ok)
|
||||
assert.Contains(t, cls, statesIcons[status])
|
||||
}
|
||||
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"})
|
||||
css := unittest.AssertExistsAndLoadBean(t, &git_model.CommitStatusSummary{RepoID: repo1.ID, SHA: commitID})
|
||||
assert.Equal(t, commitstatus.CommitStatusSuccess, css.State)
|
||||
})
|
||||
}
|
||||
|
||||
func doAPICreateCommitStatus(ctx APITestContext, commitID string, data api.CreateStatusOption) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
req := NewRequestWithJSON(
|
||||
t,
|
||||
http.MethodPost,
|
||||
fmt.Sprintf("/api/v1/repos/%s/%s/statuses/%s", ctx.Username, ctx.Reponame, commitID),
|
||||
data,
|
||||
).AddTokenAuth(ctx.Token)
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
ctx.Session.MakeRequest(t, req, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) {
|
||||
// Merge must continue if commits SHA are different, even if content is same
|
||||
// Reason: gitflow and merging master back into develop, where is high possibility, there are no changes
|
||||
@ -174,16 +79,16 @@ func TestPullCreate_EmptyChangesWithSameCommits(t *testing.T) {
|
||||
func TestPullStatusDelayCheck(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
defer test.MockVariableValue(&setting.Repository.PullRequest.DelayCheckForInactiveDays, 1)()
|
||||
defer test.MockVariableValue(&pull.AddPullRequestToCheckQueue)()
|
||||
defer test.MockVariableValue(&pull_service.AddPullRequestToCheckQueue)()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
|
||||
run := func(t *testing.T, fn func(*testing.T)) (issue3 *issues.Issue, checkedPrID int64) {
|
||||
pull.AddPullRequestToCheckQueue = func(prID int64) {
|
||||
run := func(t *testing.T, fn func(*testing.T)) (issue3 *issues_model.Issue, checkedPrID int64) {
|
||||
pull_service.AddPullRequestToCheckQueue = func(prID int64) {
|
||||
checkedPrID = prID
|
||||
}
|
||||
fn(t)
|
||||
issue3 = unittest.AssertExistsAndLoadBean(t, &issues.Issue{RepoID: 1, Index: 3})
|
||||
issue3 = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: 1, Index: 3})
|
||||
_ = issue3.LoadPullRequest(t.Context())
|
||||
return issue3, checkedPrID
|
||||
}
|
||||
@ -201,7 +106,7 @@ func TestPullStatusDelayCheck(t *testing.T) {
|
||||
|
||||
// PR issue3 is merageable at the beginning
|
||||
issue3, checkedPrID := run(t, func(t *testing.T) {})
|
||||
assert.Equal(t, issues.PullRequestStatusMergeable, issue3.PullRequest.Status)
|
||||
assert.Equal(t, issues_model.PullRequestStatusMergeable, issue3.PullRequest.Status)
|
||||
assert.Zero(t, checkedPrID)
|
||||
assertReloadingInterval(t, "") // the PR is mergeable, so no need to reload the merge box
|
||||
|
||||
@ -213,7 +118,7 @@ func TestPullStatusDelayCheck(t *testing.T) {
|
||||
issue3, checkedPrID = run(t, func(t *testing.T) {
|
||||
testEditFile(t, session, "user2", "repo1", "master", "README.md", "new content 1")
|
||||
})
|
||||
assert.Equal(t, issues.PullRequestStatusChecking, issue3.PullRequest.Status)
|
||||
assert.Equal(t, issues_model.PullRequestStatusChecking, issue3.PullRequest.Status)
|
||||
assert.Zero(t, checkedPrID)
|
||||
assertReloadingInterval(t, "2000") // the PR status is "checking", so try to reload the merge box
|
||||
|
||||
@ -222,14 +127,14 @@ func TestPullStatusDelayCheck(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/user2/repo1/pulls/3")
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
})
|
||||
assert.Equal(t, issues.PullRequestStatusChecking, issue3.PullRequest.Status)
|
||||
assert.Equal(t, issues_model.PullRequestStatusChecking, issue3.PullRequest.Status)
|
||||
assert.Equal(t, issue3.PullRequest.ID, checkedPrID)
|
||||
|
||||
// when base branch changes, still so no real check
|
||||
issue3, checkedPrID = run(t, func(t *testing.T) {
|
||||
testEditFile(t, session, "user2", "repo1", "master", "README.md", "new content 2")
|
||||
})
|
||||
assert.Equal(t, issues.PullRequestStatusChecking, issue3.PullRequest.Status)
|
||||
assert.Equal(t, issues_model.PullRequestStatusChecking, issue3.PullRequest.Status)
|
||||
assert.Zero(t, checkedPrID)
|
||||
|
||||
// then allow to check PRs without delay, when base branch changes, the PRs will be checked
|
||||
@ -237,7 +142,407 @@ func TestPullStatusDelayCheck(t *testing.T) {
|
||||
issue3, checkedPrID = run(t, func(t *testing.T) {
|
||||
testEditFile(t, session, "user2", "repo1", "master", "README.md", "new content 3")
|
||||
})
|
||||
assert.Equal(t, issues.PullRequestStatusChecking, issue3.PullRequest.Status)
|
||||
assert.Equal(t, issues_model.PullRequestStatusChecking, issue3.PullRequest.Status)
|
||||
assert.Equal(t, issue3.PullRequest.ID, checkedPrID)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_PullRequestStatusChecking_Mergeable_MergeTree(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, true)()
|
||||
testPullRequestStatusCheckingMergeable(t)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_PullRequestStatusChecking_Mergeable_TmpRepo(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, false)()
|
||||
testPullRequestStatusCheckingMergeable(t)
|
||||
})
|
||||
}
|
||||
|
||||
func testPullRequestStatusCheckingMergeable(t *testing.T) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
session := loginUser(t, user.Name)
|
||||
|
||||
// Create new clean repo to test conflict checking.
|
||||
baseRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{
|
||||
Name: "conflict-checking",
|
||||
Description: "Tempo repo",
|
||||
AutoInit: true,
|
||||
Readme: "Default",
|
||||
DefaultBranch: "main",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, baseRepo)
|
||||
|
||||
// create a commit on new branch.
|
||||
testCreateFile(t, session, baseRepo.OwnerName, baseRepo.Name, "main", "important-secrets", "important_file", "Just a non-important file")
|
||||
|
||||
// create Pull to merge the important-secrets branch into main branch.
|
||||
resp := testPullCreateDirectly(t, session, createPullRequestOptions{
|
||||
BaseRepoOwner: baseRepo.OwnerName,
|
||||
BaseRepoName: baseRepo.Name,
|
||||
BaseBranch: "main",
|
||||
HeadRepoOwner: baseRepo.OwnerName,
|
||||
HeadRepoName: baseRepo.Name,
|
||||
HeadBranch: "important-secrets",
|
||||
Title: "PR with no conflict",
|
||||
})
|
||||
// check the redirected URL
|
||||
url := test.RedirectURL(resp)
|
||||
assert.Regexp(t, fmt.Sprintf("^/%s/pulls/[0-9]*$", baseRepo.FullName()), url)
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with no conflict"})
|
||||
assert.NoError(t, issue.LoadPullRequest(t.Context()))
|
||||
conflictingPR := issue.PullRequest
|
||||
|
||||
// Ensure conflictedFiles is populated.
|
||||
assert.Empty(t, conflictingPR.ConflictedFiles)
|
||||
// Check if status is correct.
|
||||
assert.Equal(t, issues_model.PullRequestStatusMergeable, conflictingPR.Status)
|
||||
// Ensure that mergeable returns true
|
||||
assert.True(t, conflictingPR.Mergeable(t.Context()))
|
||||
}
|
||||
|
||||
func Test_PullRequestStatusChecking_Conflicted_MergeTree(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, true)()
|
||||
testPullRequestStatusCheckingConflicted(t)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_PullRequestStatusChecking_Conflicted_TmpRepo(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, false)()
|
||||
testPullRequestStatusCheckingConflicted(t)
|
||||
})
|
||||
}
|
||||
|
||||
func testPullRequestStatusCheckingConflicted(t *testing.T) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
session := loginUser(t, user.Name)
|
||||
|
||||
// Create new clean repo to test conflict checking.
|
||||
baseRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{
|
||||
Name: "conflict-checking",
|
||||
Description: "Tempo repo",
|
||||
AutoInit: true,
|
||||
Readme: "Default",
|
||||
DefaultBranch: "main",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, baseRepo)
|
||||
|
||||
// create a commit on new branch.
|
||||
testCreateFile(t, session, baseRepo.OwnerName, baseRepo.Name, "main", "important-secrets", "important_file", "Just a non-important file")
|
||||
|
||||
// create a commit on main branch.
|
||||
testCreateFile(t, session, baseRepo.OwnerName, baseRepo.Name, "main", "main", "important_file", "Not the same content :P")
|
||||
|
||||
// create Pull to merge the important-secrets branch into main branch.
|
||||
resp := testPullCreateDirectly(t, session, createPullRequestOptions{
|
||||
BaseRepoOwner: baseRepo.OwnerName,
|
||||
BaseRepoName: baseRepo.Name,
|
||||
BaseBranch: "main",
|
||||
HeadRepoOwner: baseRepo.OwnerName,
|
||||
HeadRepoName: baseRepo.Name,
|
||||
HeadBranch: "important-secrets",
|
||||
Title: "PR with conflict!",
|
||||
})
|
||||
// check the redirected URL
|
||||
url := test.RedirectURL(resp)
|
||||
assert.Regexp(t, fmt.Sprintf("^/%s/pulls/[0-9]*$", baseRepo.FullName()), url)
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"})
|
||||
assert.NoError(t, issue.LoadPullRequest(t.Context()))
|
||||
conflictingPR := issue.PullRequest
|
||||
|
||||
// Ensure conflictedFiles is populated.
|
||||
assert.Equal(t, []string{"important_file"}, conflictingPR.ConflictedFiles)
|
||||
// Check if status is correct.
|
||||
assert.Equal(t, issues_model.PullRequestStatusConflict, conflictingPR.Status)
|
||||
// Ensure that mergeable returns false
|
||||
assert.False(t, conflictingPR.Mergeable(t.Context()))
|
||||
}
|
||||
|
||||
func Test_PullRequestStatusCheckingCrossRepo_Mergeable_MergeTree(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, true)()
|
||||
testPullRequestStatusCheckingCrossRepoMergeable(t, giteaURL)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_PullRequestStatusCheckingCrossRepo_Mergeable_TmpRepo(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, false)()
|
||||
testPullRequestStatusCheckingCrossRepoMergeable(t, giteaURL)
|
||||
})
|
||||
}
|
||||
|
||||
func testPullRequestStatusCheckingCrossRepoMergeable(t *testing.T, giteaURL *url.URL) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
session := loginUser(t, user.Name)
|
||||
|
||||
// Create new clean repo to test conflict checking.
|
||||
baseRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{
|
||||
Name: "conflict-checking",
|
||||
Description: "Tempo repo",
|
||||
AutoInit: true,
|
||||
Readme: "Default",
|
||||
DefaultBranch: "main",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, baseRepo)
|
||||
|
||||
testRepoFork(t, session, baseRepo.OwnerName, baseRepo.Name, "org3", "conflict-checking", "main")
|
||||
|
||||
forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "org3", Name: "conflict-checking"})
|
||||
|
||||
// create a commit on new branch of forked repository
|
||||
testCreateFile(t, session, forkRepo.OwnerName, forkRepo.Name, "main", "important-secrets", "important_file", "Just a non-important file")
|
||||
|
||||
// create Pull to merge the important-secrets branch into main branch.
|
||||
resp := testPullCreateDirectly(t, session, createPullRequestOptions{
|
||||
BaseRepoOwner: baseRepo.OwnerName,
|
||||
BaseRepoName: baseRepo.Name,
|
||||
BaseBranch: "main",
|
||||
HeadRepoOwner: forkRepo.OwnerName,
|
||||
HeadRepoName: forkRepo.Name,
|
||||
HeadBranch: "important-secrets",
|
||||
Title: "PR with no conflict",
|
||||
})
|
||||
// check the redirected URL
|
||||
url := test.RedirectURL(resp)
|
||||
assert.Regexp(t, fmt.Sprintf("^/%s/pulls/[0-9]*$", baseRepo.FullName()), url)
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with no conflict"})
|
||||
assert.NoError(t, issue.LoadPullRequest(t.Context()))
|
||||
conflictingPR := issue.PullRequest
|
||||
|
||||
// Ensure conflictedFiles is populated.
|
||||
assert.Empty(t, conflictingPR.ConflictedFiles)
|
||||
// Check if status is correct.
|
||||
assert.Equal(t, issues_model.PullRequestStatusMergeable, conflictingPR.Status)
|
||||
// Ensure that mergeable returns true
|
||||
assert.True(t, conflictingPR.Mergeable(t.Context()))
|
||||
}
|
||||
|
||||
func Test_PullRequestStatusCheckingCrossRepo_Conflicted_MergeTree(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, true)()
|
||||
testPullRequestStatusCheckingCrossRepoConflicted(t, giteaURL)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_PullRequestStatusCheckingCrossRepo_Conflicted_TmpRepo(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, false)()
|
||||
testPullRequestStatusCheckingCrossRepoConflicted(t, giteaURL)
|
||||
})
|
||||
}
|
||||
|
||||
func testPullRequestStatusCheckingCrossRepoConflicted(t *testing.T, giteaURL *url.URL) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
session := loginUser(t, user.Name)
|
||||
|
||||
// Create new clean repo to test conflict checking.
|
||||
baseRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{
|
||||
Name: "conflict-checking",
|
||||
Description: "Tempo repo",
|
||||
AutoInit: true,
|
||||
Readme: "Default",
|
||||
DefaultBranch: "main",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, baseRepo)
|
||||
|
||||
testRepoFork(t, session, baseRepo.OwnerName, baseRepo.Name, "org3", "conflict-checking", "main")
|
||||
|
||||
forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "org3", Name: "conflict-checking"})
|
||||
|
||||
// create a commit on new branch of forked repository
|
||||
testCreateFile(t, session, forkRepo.OwnerName, forkRepo.Name, "main", "important-secrets", "important_file", "Just a non-important file")
|
||||
|
||||
// create a commit on main branch of base repository.
|
||||
testCreateFile(t, session, baseRepo.OwnerName, baseRepo.Name, "main", "main", "important_file", "Not the same content :P")
|
||||
|
||||
// create Pull to merge the important-secrets branch into main branch.
|
||||
resp := testPullCreateDirectly(t, session, createPullRequestOptions{
|
||||
BaseRepoOwner: baseRepo.OwnerName,
|
||||
BaseRepoName: baseRepo.Name,
|
||||
BaseBranch: "main",
|
||||
HeadRepoOwner: forkRepo.OwnerName,
|
||||
HeadRepoName: forkRepo.Name,
|
||||
HeadBranch: "important-secrets",
|
||||
Title: "PR with conflict!",
|
||||
})
|
||||
// check the redirected URL
|
||||
url := test.RedirectURL(resp)
|
||||
assert.Regexp(t, fmt.Sprintf("^/%s/pulls/[0-9]*$", baseRepo.FullName()), url)
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"})
|
||||
assert.NoError(t, issue.LoadPullRequest(t.Context()))
|
||||
conflictingPR := issue.PullRequest
|
||||
|
||||
// Ensure conflictedFiles is populated.
|
||||
assert.Equal(t, []string{"important_file"}, conflictingPR.ConflictedFiles)
|
||||
// Check if status is correct.
|
||||
assert.Equal(t, issues_model.PullRequestStatusConflict, conflictingPR.Status)
|
||||
// Ensure that mergeable returns false
|
||||
assert.False(t, conflictingPR.Mergeable(t.Context()))
|
||||
}
|
||||
|
||||
func Test_PullRequest_AGit_StatusChecking_Mergeable_MergeTree(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, true)()
|
||||
|
||||
testPullRequestAGitStatusCheckingMergeable(t, giteaURL)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_PullRequest_AGit_StatusChecking_Mergeable_TmpRepo(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, false)()
|
||||
|
||||
testPullRequestAGitStatusCheckingMergeable(t, giteaURL)
|
||||
})
|
||||
}
|
||||
|
||||
func testPullRequestAGitStatusCheckingMergeable(t *testing.T, giteaURL *url.URL) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
// Create new clean repo to test conflict checking.
|
||||
baseRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{
|
||||
Name: "conflict-checking",
|
||||
Description: "Tempo repo",
|
||||
AutoInit: true,
|
||||
Readme: "Default",
|
||||
DefaultBranch: "main",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, baseRepo)
|
||||
|
||||
// add something in local repository and push it to remote
|
||||
dstPath := t.TempDir()
|
||||
repoURL := *giteaURL
|
||||
repoURL.Path = baseRepo.FullName() + ".git"
|
||||
repoURL.User = url.UserPassword("user2", userPassword)
|
||||
doGitClone(dstPath, &repoURL)(t)
|
||||
|
||||
gitRepo, err := git.OpenRepository(t.Context(), dstPath)
|
||||
assert.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
|
||||
doGitCreateBranch(dstPath, "test-agit-push")(t)
|
||||
|
||||
_, err = generateCommitWithNewData(t.Context(), testFileSizeSmall, dstPath, "user2@example.com", "User Two", "branch-data-file-")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// push to create an agit pull request
|
||||
err = gitcmd.NewCommand("push", "origin",
|
||||
"-o", "title=agit-test-title", "-o", "description=agit-test-description",
|
||||
"-o", "topic=head-branch-name",
|
||||
"HEAD:refs/for/main",
|
||||
).WithDir(dstPath).Run(t.Context())
|
||||
assert.NoError(t, err)
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
|
||||
RepoID: baseRepo.ID,
|
||||
Title: "agit-test-title",
|
||||
})
|
||||
assert.NoError(t, issue.LoadPullRequest(t.Context()))
|
||||
conflictingPR := issue.PullRequest
|
||||
|
||||
// Ensure conflictedFiles is populated.
|
||||
assert.Empty(t, conflictingPR.ConflictedFiles)
|
||||
// Check if status is correct.
|
||||
assert.Equal(t, issues_model.PullRequestStatusMergeable, conflictingPR.Status)
|
||||
// Ensure that mergeable returns true
|
||||
assert.True(t, conflictingPR.Mergeable(t.Context()))
|
||||
}
|
||||
|
||||
func Test_PullRequest_AGit_StatusChecking_Conflicted_MergeTree(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, true)()
|
||||
|
||||
testPullRequestAGitStatusCheckingConflicted(t, giteaURL)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_PullRequest_AGit_StatusChecking_Conflicted_TmpRepo(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, false)()
|
||||
|
||||
testPullRequestAGitStatusCheckingConflicted(t, giteaURL)
|
||||
})
|
||||
}
|
||||
|
||||
func testPullRequestAGitStatusCheckingConflicted(t *testing.T, giteaURL *url.URL) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
// Create new clean repo to test conflict checking.
|
||||
baseRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{
|
||||
Name: "conflict-checking",
|
||||
Description: "Tempo repo",
|
||||
AutoInit: true,
|
||||
Readme: "Default",
|
||||
DefaultBranch: "main",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, baseRepo)
|
||||
|
||||
// add something in local repository and push it to remote
|
||||
dstPath := t.TempDir()
|
||||
repoURL := *giteaURL
|
||||
repoURL.Path = baseRepo.FullName() + ".git"
|
||||
repoURL.User = url.UserPassword("user2", userPassword)
|
||||
doGitClone(dstPath, &repoURL)(t)
|
||||
|
||||
gitRepo, err := git.OpenRepository(t.Context(), dstPath)
|
||||
assert.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
|
||||
// create agit branch from current commit
|
||||
doGitCreateBranch(dstPath, "test-agit-push")(t)
|
||||
|
||||
doGitCheckoutWriteFileCommit(localGitAddCommitOptions{
|
||||
LocalRepoPath: dstPath,
|
||||
CheckoutBranch: "main",
|
||||
TreeFilePath: "README.md",
|
||||
TreeFileContent: "Some changes to README file to main cause conflict",
|
||||
})(t)
|
||||
|
||||
err = gitcmd.NewCommand("push", "origin", "main").WithDir(dstPath).Run(t.Context())
|
||||
assert.NoError(t, err)
|
||||
|
||||
// check out back to agit branch and change the same file
|
||||
doGitCheckoutWriteFileCommit(localGitAddCommitOptions{
|
||||
LocalRepoPath: dstPath,
|
||||
CheckoutBranch: "test-agit-push",
|
||||
TreeFilePath: "README.md",
|
||||
TreeFileContent: "Some changes to README file for agit branch",
|
||||
})(t)
|
||||
|
||||
// push to create an agit pull request
|
||||
err = gitcmd.NewCommand("push", "origin",
|
||||
"-o", "title=agit-test-title", "-o", "description=agit-test-description",
|
||||
"-o", "topic=head-branch-name",
|
||||
"HEAD:refs/for/main",
|
||||
).WithDir(dstPath).Run(t.Context())
|
||||
assert.NoError(t, err)
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
|
||||
RepoID: baseRepo.ID,
|
||||
Title: "agit-test-title",
|
||||
})
|
||||
assert.NoError(t, issue.LoadPullRequest(t.Context()))
|
||||
conflictingPR := issue.PullRequest
|
||||
|
||||
// Ensure conflictedFiles is populated.
|
||||
assert.Equal(t, []string{"README.md"}, conflictingPR.ConflictedFiles)
|
||||
// Check if status is correct.
|
||||
assert.Equal(t, issues_model.PullRequestStatusConflict, conflictingPR.Status)
|
||||
// Ensure that mergeable returns false
|
||||
assert.False(t, conflictingPR.Mergeable(t.Context()))
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user