1// Copyright 2019 The Gitea Authors.
2// All rights reserved.
3// SPDX-License-Identifier: MIT
4
5package pull
6
7import (
8 "context"
9 "errors"
10 "fmt"
11
12 "forgejo.org/models/db"
13 git_model "forgejo.org/models/git"
14 issues_model "forgejo.org/models/issues"
15 "forgejo.org/modules/git"
16 "forgejo.org/modules/gitrepo"
17 "forgejo.org/modules/log"
18 "forgejo.org/modules/structs"
19
20 "github.com/gobwas/glob"
21)
22
23// MergeRequiredContextsCommitStatus returns a commit status state for given required contexts
24func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus, requiredContexts []string) structs.CommitStatusState {
25 // matchedCount is the number of `CommitStatus.Context` that match any context of `requiredContexts`
26 matchedCount := 0
27 returnedStatus := structs.CommitStatusSuccess
28
29 if len(requiredContexts) > 0 {
30 requiredContextsGlob := make(map[string]glob.Glob, len(requiredContexts))
31 for _, ctx := range requiredContexts {
32 if gp, err := glob.Compile(ctx); err != nil {
33 log.Error("glob.Compile %s failed. Error: %v", ctx, err)
34 } else {
35 requiredContextsGlob[ctx] = gp
36 }
37 }
38
39 for _, gp := range requiredContextsGlob {
40 var targetStatus structs.CommitStatusState
41 for _, commitStatus := range commitStatuses {
42 if gp.Match(commitStatus.Context) {
43 targetStatus = commitStatus.State
44 matchedCount++
45 break
46 }
47 }
48
49 // If required rule not match any action, then it is pending
50 if targetStatus == "" {
51 if structs.CommitStatusPending.NoBetterThan(returnedStatus) {
52 returnedStatus = structs.CommitStatusPending
53 }
54 break
55 }
56
57 if targetStatus.NoBetterThan(returnedStatus) {
58 returnedStatus = targetStatus
59 }
60 }
61 }
62
63 if matchedCount == 0 && returnedStatus == structs.CommitStatusSuccess {
64 status := git_model.CalcCommitStatus(commitStatuses)
65 if status != nil {
66 return status.State
67 }
68 return ""
69 }
70
71 return returnedStatus
72}
73
74// IsPullCommitStatusPass returns if all required status checks PASS
75func IsPullCommitStatusPass(ctx context.Context, pr *issues_model.PullRequest) (bool, error) {
76 pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
77 if err != nil {
78 return false, fmt.Errorf("GetFirstMatchProtectedBranchRule: %w", err)
79 }
80 if pb == nil || !pb.EnableStatusCheck {
81 return true, nil
82 }
83
84 state, err := GetPullRequestCommitStatusState(ctx, pr)
85 if err != nil {
86 return false, err
87 }
88 return state.IsSuccess(), nil
89}
90
91// GetPullRequestCommitStatusState returns pull request merged commit status state
92func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullRequest) (structs.CommitStatusState, error) {
93 // Ensure HeadRepo is loaded
94 if err := pr.LoadHeadRepo(ctx); err != nil {
95 return "", fmt.Errorf("LoadHeadRepo: %w", err)
96 }
97
98 // check if all required status checks are successful
99 headGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.HeadRepo)
100 if err != nil {
101 return "", fmt.Errorf("RepositoryFromContextOrOpen: %w", err)
102 }
103 defer closer.Close()
104
105 if pr.Flow == issues_model.PullRequestFlowGithub && !headGitRepo.IsBranchExist(pr.HeadBranch) {
106 return "", errors.New("head branch does not exist, can not merge")
107 }
108 if pr.Flow == issues_model.PullRequestFlowAGit && !git.IsReferenceExist(ctx, headGitRepo.Path, pr.GetGitRefName()) {
109 return "", errors.New("head branch does not exist, can not merge")
110 }
111
112 var sha string
113 if pr.Flow == issues_model.PullRequestFlowGithub {
114 sha, err = headGitRepo.GetBranchCommitID(pr.HeadBranch)
115 } else {
116 sha, err = headGitRepo.GetRefCommitID(pr.GetGitRefName())
117 }
118 if err != nil {
119 return "", err
120 }
121
122 if err := pr.LoadBaseRepo(ctx); err != nil {
123 return "", fmt.Errorf("LoadBaseRepo: %w", err)
124 }
125
126 commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptionsAll)
127 if err != nil {
128 return "", fmt.Errorf("GetLatestCommitStatus: %w", err)
129 }
130
131 pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
132 if err != nil {
133 return "", fmt.Errorf("GetFirstMatchProtectedBranchRule: %w", err)
134 }
135 var requiredContexts []string
136 if pb != nil {
137 requiredContexts = pb.StatusCheckContexts
138 }
139
140 return MergeRequiredContextsCommitStatus(commitStatuses, requiredContexts), nil
141}