|
| 1 | +// Copyright 2025 The Gitea Authors. All rights reserved. |
| 2 | +// SPDX-License-Identifier: MIT |
| 3 | + |
| 4 | +package pull |
| 5 | + |
| 6 | +import ( |
| 7 | + "context" |
| 8 | + "errors" |
| 9 | + "fmt" |
| 10 | + |
| 11 | + issues_model "code.gitea.io/gitea/models/issues" |
| 12 | + "code.gitea.io/gitea/modules/git" |
| 13 | + "code.gitea.io/gitea/modules/git/gitcmd" |
| 14 | + "code.gitea.io/gitea/modules/gitrepo" |
| 15 | + "code.gitea.io/gitea/modules/log" |
| 16 | +) |
| 17 | + |
| 18 | +// checkPullRequestMergeableAndUpdateStatus checks whether a pull request is mergeable and updates its status accordingly. |
| 19 | +// It uses 'git merge-tree' if supported by the Git version, otherwise it falls back to using a temporary repository. |
| 20 | +// This function updates the pr.Status, pr.MergeBase and pr.ConflictedFiles fields as necessary. |
| 21 | +func checkPullRequestMergeableAndUpdateStatus(ctx context.Context, pr *issues_model.PullRequest) error { |
| 22 | + if git.DefaultFeatures().SupportGitMergeTree { |
| 23 | + return checkPullRequestMergeableAndUpdateStatusMergeTree(ctx, pr) |
| 24 | + } |
| 25 | + |
| 26 | + return checkPullRequestMergeableAndUpdateStatusTmpRepo(ctx, pr) |
| 27 | +} |
| 28 | + |
| 29 | +// checkConflictsMergeTree uses git merge-tree to check for conflicts and if none are found checks if the patch is empty |
| 30 | +// return true if there is conflicts otherwise return false |
| 31 | +// pr.Status and pr.ConflictedFiles will be updated as necessary |
| 32 | +func checkConflictsMergeTree(ctx context.Context, repoPath string, pr *issues_model.PullRequest, baseCommitID string) (bool, error) { |
| 33 | + treeHash, conflict, conflictFiles, err := gitrepo.MergeTree(ctx, pr.BaseRepo, pr.MergeBase, baseCommitID, pr.HeadCommitID) |
| 34 | + if err != nil { |
| 35 | + return false, fmt.Errorf("MergeTree: %w", err) |
| 36 | + } |
| 37 | + if conflict { |
| 38 | + pr.Status = issues_model.PullRequestStatusConflict |
| 39 | + pr.ConflictedFiles = conflictFiles |
| 40 | + |
| 41 | + log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles) |
| 42 | + return true, nil |
| 43 | + } |
| 44 | + |
| 45 | + // No conflicts were detected, now check if the pull request actually |
| 46 | + // contains anything useful via a diff. git-diff-tree(1) with --quiet |
| 47 | + // will return exit code 0 if there's no diff and exit code 1 if there's |
| 48 | + // a diff. |
| 49 | + isEmpty := true |
| 50 | + if err = gitcmd.NewCommand("diff-tree", "--quiet").AddDynamicArguments(treeHash, pr.MergeBase). |
| 51 | + Run(ctx, &gitcmd.RunOpts{ |
| 52 | + Dir: repoPath, |
| 53 | + }); err != nil { |
| 54 | + if !gitcmd.IsErrorExitCode(err, 1) { |
| 55 | + return false, fmt.Errorf("DiffTree: %w", err) |
| 56 | + } |
| 57 | + isEmpty = false |
| 58 | + } |
| 59 | + |
| 60 | + if isEmpty { |
| 61 | + log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID) |
| 62 | + pr.Status = issues_model.PullRequestStatusEmpty |
| 63 | + } |
| 64 | + return false, nil |
| 65 | +} |
| 66 | + |
| 67 | +func checkPullRequestMergeableAndUpdateStatusMergeTree(ctx context.Context, pr *issues_model.PullRequest) error { |
| 68 | + // 1. Get head commit |
| 69 | + if err := pr.LoadHeadRepo(ctx); err != nil { |
| 70 | + return err |
| 71 | + } |
| 72 | + headGitRepo, err := gitrepo.OpenRepository(ctx, pr.HeadRepo) |
| 73 | + if err != nil { |
| 74 | + return fmt.Errorf("OpenRepository: %w", err) |
| 75 | + } |
| 76 | + defer headGitRepo.Close() |
| 77 | + |
| 78 | + if pr.Flow == issues_model.PullRequestFlowGithub { |
| 79 | + pr.HeadCommitID, err = headGitRepo.GetRefCommitID(git.BranchPrefix + pr.HeadBranch) |
| 80 | + if err != nil { |
| 81 | + return fmt.Errorf("GetBranchCommitID: can't find commit ID for head: %w", err) |
| 82 | + } |
| 83 | + } else if pr.HeadCommitID == "" { |
| 84 | + return errors.New("head commit ID is empty for pull request Agit flow") |
| 85 | + } |
| 86 | + |
| 87 | + // 2. Get base commit id |
| 88 | + var baseGitRepo *git.Repository |
| 89 | + if pr.IsSameRepo() { |
| 90 | + baseGitRepo = headGitRepo |
| 91 | + } else { |
| 92 | + baseGitRepo, err = gitrepo.OpenRepository(ctx, pr.BaseRepo) |
| 93 | + if err != nil { |
| 94 | + return fmt.Errorf("OpenRepository: %w", err) |
| 95 | + } |
| 96 | + defer baseGitRepo.Close() |
| 97 | + // 2.1. fetch head commit id into the current repository |
| 98 | + // it will be checked in 2 weeks by default from git if the pull request created failure. |
| 99 | + if err := gitrepo.FetchRemoteCommit(ctx, pr.BaseRepo, pr.HeadRepo, pr.HeadCommitID); err != nil { |
| 100 | + return fmt.Errorf("FetchRemoteCommit: %w", err) |
| 101 | + } |
| 102 | + } |
| 103 | + baseCommitID, err := baseGitRepo.GetRefCommitID(git.BranchPrefix + pr.BaseBranch) |
| 104 | + if err != nil { |
| 105 | + return fmt.Errorf("GetBranchCommitID: can't find commit ID for base: %w", err) |
| 106 | + } |
| 107 | + |
| 108 | + // 3. update merge base |
| 109 | + pr.MergeBase, err = gitrepo.MergeBase(ctx, pr.BaseRepo, baseCommitID, pr.HeadCommitID) |
| 110 | + if err != nil { |
| 111 | + log.Error("GetMergeBase: %v and can't find commit ID for base: %v", err, baseCommitID) |
| 112 | + // FIXME: is this the right thing to do? |
| 113 | + pr.MergeBase = baseCommitID |
| 114 | + } |
| 115 | + |
| 116 | + // 4. if base == head, then it's an ancestor |
| 117 | + if pr.HeadCommitID == pr.MergeBase { |
| 118 | + pr.Status = issues_model.PullRequestStatusAncestor |
| 119 | + return nil |
| 120 | + } |
| 121 | + |
| 122 | + // 5. Check for conflicts |
| 123 | + conflicted, err := checkConflictsMergeTree(ctx, pr.BaseRepo.RepoPath(), pr, baseCommitID) |
| 124 | + if err != nil { |
| 125 | + return fmt.Errorf("checkConflictsMergeTree: %w", err) |
| 126 | + } |
| 127 | + if conflicted || pr.Status == issues_model.PullRequestStatusEmpty { |
| 128 | + return nil |
| 129 | + } |
| 130 | + |
| 131 | + // 6. Check for protected files changes |
| 132 | + if err = checkPullFilesProtection(ctx, pr, pr.BaseRepo.RepoPath()); err != nil { |
| 133 | + return fmt.Errorf("pr.CheckPullFilesProtection(): %v", err) |
| 134 | + } |
| 135 | + if len(pr.ChangedProtectedFiles) > 0 { |
| 136 | + log.Trace("Found %d protected files changed", len(pr.ChangedProtectedFiles)) |
| 137 | + } |
| 138 | + |
| 139 | + pr.Status = issues_model.PullRequestStatusMergeable |
| 140 | + return nil |
| 141 | +} |
0 commit comments