mirror of
https://github.com/go-gitea/gitea.git
synced 2026-01-30 11:19:39 +01:00
In Git 2.38, the `merge-tree` command introduced the `--write-tree` option, which works directly on bare repositories. In Git 2.40, a new parameter `--merge-base` introduced so we require Git 2.40 to use the merge tree feature. This option produces the merged tree object ID, allowing us to perform diffs between commits without creating a temporary repository. By avoiding the overhead of setting up and tearing down temporary repos, this approach delivers a notable performance improvement. It also fixes a possible situation that conflict files might be empty but it's a conflict status according to https://git-scm.com/docs/git-merge-tree#_mistakes_to_avoid Replace #35542 --------- Signed-off-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
145 lines
5.1 KiB
Go
145 lines
5.1 KiB
Go
// Copyright 2026 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"
|
|
)
|
|
|
|
// checkConflictsMergeTree uses git merge-tree to check for conflicts and if none are found checks if the patch is empty
|
|
// return true if there are 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
|
|
// sometimes git merge-tree will detect conflicts but not list any conflicted files
|
|
// so that pr.ConflictedFiles will be empty
|
|
pr.ConflictedFiles = conflictFiles
|
|
|
|
log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles)
|
|
return true, nil
|
|
}
|
|
|
|
// Detect whether the pull request introduces changes by comparing the merged tree (treeHash)
|
|
// against the current base commit (baseCommitID) using `git diff-tree`. The command returns exit code 0
|
|
// if there is no diff between these trees (empty patch) and exit code 1 if there is a diff.
|
|
gitErr := gitrepo.RunCmd(ctx, pr.BaseRepo, gitcmd.NewCommand("diff-tree", "-r", "--quiet").
|
|
AddDynamicArguments(treeHash, baseCommitID))
|
|
switch {
|
|
case gitErr == nil:
|
|
log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID)
|
|
pr.Status = issues_model.PullRequestStatusEmpty
|
|
case gitcmd.IsErrorExitCode(gitErr, 1):
|
|
pr.Status = issues_model.PullRequestStatusMergeable
|
|
default:
|
|
return false, fmt.Errorf("run diff-tree exit abnormally: %w", gitErr)
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func checkPullRequestMergeableByMergeTree(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/open base repository
|
|
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 !baseGitRepo.IsReferenceExist(pr.HeadCommitID) {
|
|
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 {
|
|
// if there is no merge base, then it's empty, still need to allow the pull request to be created
|
|
// not quite right (e.g.: why not reset the fields like below), but no interest to do more investigation at the moment
|
|
log.Error("MergeBase: unable to find merge base between %s and %s: %v", baseCommitID, pr.HeadCommitID, err)
|
|
pr.Status = issues_model.PullRequestStatusEmpty
|
|
return nil
|
|
}
|
|
|
|
// reset conflicted files and changed protected files
|
|
pr.ConflictedFiles = nil
|
|
pr.ChangedProtectedFiles = 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.PullRequestStatusError
|
|
return fmt.Errorf("checkConflictsMergeTree: %w", err)
|
|
}
|
|
if conflicted || pr.Status == issues_model.PullRequestStatusEmpty {
|
|
return nil
|
|
}
|
|
|
|
// 8. Check for protected files changes
|
|
if err = checkPullFilesProtection(ctx, pr, baseGitRepo, pr.HeadCommitID); err != nil {
|
|
return fmt.Errorf("checkPullFilesProtection: %w", err)
|
|
}
|
|
return nil
|
|
}
|