-
Notifications
You must be signed in to change notification settings - Fork 117
/
pullrequest.go
225 lines (214 loc) · 6.67 KB
/
pullrequest.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
package promotion
import (
"context"
"fmt"
"strconv"
kargoapi "github.com/akuity/kargo/api/v1alpha1"
"github.com/akuity/kargo/internal/controller/git"
"github.com/akuity/kargo/internal/gitprovider"
"github.com/akuity/kargo/internal/gitprovider/github"
"github.com/akuity/kargo/internal/gitprovider/gitlab"
)
func pullRequestBranchName(project, stage string) string {
return fmt.Sprintf("kargo/%s/%s/promotion", project, stage)
}
// preparePullRequestBranch prepares a branch to be used as the pull request branch for
// merging into the base branch. If the PR branch already exists, but not in a state that
// we like (i.e. not a descendant of base), recreate it.
func preparePullRequestBranch(repo git.Repo, prBranch string, base string) error {
origBranch := repo.CurrentBranch()
baseBranchExists, err := repo.RemoteBranchExists(base)
if err != nil {
return err
}
if !baseBranchExists {
// Base branch doesn't exist. Create it!
if err = repo.CreateOrphanedBranch(base); err != nil {
return err
}
if err = repo.Commit(
"Initial commit",
&git.CommitOptions{
AllowEmpty: true,
},
); err != nil {
return err
}
if err = repo.Push(false); err != nil {
return err
}
} else if err = repo.Checkout(base); err != nil {
return err
}
prBranchExists, err := repo.RemoteBranchExists(prBranch)
if err != nil {
return err
}
if !prBranchExists {
// PR branch doesn't exist
if err := repo.CreateChildBranch(prBranch); err != nil {
return err
}
if err := repo.Push(false); err != nil {
return err
}
} else {
// PR branch exists, ensure writeBranch is an ancestor.
// otherwise PRs cannot be created.
if err := repo.Checkout(prBranch); err != nil {
return err
}
isAncestor, err := repo.IsAncestor(base, prBranch)
if err != nil {
return err
}
if !isAncestor {
// Branch exists, but is not an ancestor of writeBranch, recreate it
if err = repo.Checkout(base); err != nil {
return err
}
if err = repo.DeleteBranch(prBranch); err != nil {
return err
}
if err = repo.CreateChildBranch(prBranch); err != nil {
return err
}
if err = repo.Push(true); err != nil {
return err
}
}
}
// Return to original branch
return repo.Checkout(origBranch)
}
// newGitProvider returns the appropriate git provider either if it was explicitly specified,
// or if we can infer it from the repo URL.
func newGitProvider(
url string,
pullRequest *kargoapi.PullRequestPromotionMechanism,
creds *git.RepoCredentials,
) (gitprovider.GitProviderService, error) {
var gpClient gitprovider.GitProviderService
var err error
switch {
case pullRequest.GitHub != nil:
gpClient, err = gitprovider.NewGitProviderServiceFromName(github.GitProviderServiceName)
case pullRequest.GitLab != nil:
gpClient, err = gitprovider.NewGitProviderServiceFromName(gitlab.GitProviderServiceName)
default:
gpClient, err = gitprovider.NewGitProviderServiceFromURL(url)
}
if err == nil && creds != nil {
gpClient, err = gpClient.WithAuthToken(creds.Password)
}
if err != nil {
return nil, err
}
return gpClient, nil
}
// reconcilePullRequest creates and monitors a pull request for the promotion,
// then returns a PromotionStatus reflecting current status adding metadata
// it tracks (i.e. PR url).
func reconcilePullRequest(
ctx context.Context,
status kargoapi.PromotionStatus,
repo git.Repo,
gpClient gitprovider.GitProviderService,
prBranch string,
writeBranch string,
) (string, *kargoapi.PromotionStatus, error) {
newStatus := status.DeepCopy()
var mergeCommitSHA string
prNumber := getPullRequestNumberFromMetadata(status.Metadata, repo.URL())
if prNumber == -1 {
needsPR, err := repo.RefsHaveDiffs(prBranch, writeBranch)
if err != nil {
return "", nil, err
}
if needsPR {
title, err := repo.CommitMessage(prBranch)
if err != nil {
return "", nil, err
}
createOpts := gitprovider.CreatePullRequestOpts{
Head: prBranch,
Base: writeBranch,
Title: title,
}
pr, err := gpClient.CreatePullRequest(ctx, repo.URL(), createOpts)
if err != nil {
// Error might be "A pull request already exists" for same branches.
// Check if that is the case, and reuse the existing PR if it is
prs, listErr := gpClient.ListPullRequests(ctx, repo.URL(), gitprovider.ListPullRequestOpts{
Head: prBranch,
Base: writeBranch,
})
if listErr != nil || len(prs) != 1 {
return "", nil, err
}
// If we get here, we found an existing open PR for the same branches
pr = prs[0]
}
newStatus.Phase = kargoapi.PromotionPhaseRunning
newStatus.Metadata = setPullRequestMetadata(newStatus.Metadata, repo.URL(), pr.Number, pr.URL)
} else {
newStatus.Phase = kargoapi.PromotionPhaseSucceeded
newStatus.Message = "No changes to promote"
}
} else {
// check if existing PR is closed/merged and update promo status to either
// Succeeded or Failed depending if PR was merged
pr, err := gpClient.GetPullRequest(ctx, repo.URL(), prNumber)
if err != nil {
return "", nil, err
}
if !pr.IsOpen() {
merged, err := gpClient.IsPullRequestMerged(ctx, repo.URL(), prNumber)
if err != nil {
return "", nil, err
}
if merged {
newStatus.Phase = kargoapi.PromotionPhaseSucceeded
newStatus.Message = "Pull request was merged"
if pr.MergeCommitSHA == "" {
return "", nil, fmt.Errorf("merge commit SHA is empty")
}
mergeCommitSHA = pr.MergeCommitSHA
} else {
newStatus.Phase = kargoapi.PromotionPhaseFailed
newStatus.Message = "Pull request was closed without being merged"
}
}
}
return mergeCommitSHA, newStatus, nil
}
// pullRequestMetadataKey returns the key used to store the pull request number in the metadata map.
func pullRequestMetadataKey(repoURL string) string {
return fmt.Sprintf("pr:%s", repoURL)
}
// setPullRequestMetadata sets pull request bookkeeping information to the metadata map.
func setPullRequestMetadata(metadata map[string]string, repoURL string, number int64, url string) map[string]string {
if metadata == nil {
metadata = make(map[string]string)
}
metadata[pullRequestMetadataKey(repoURL)] = strconv.FormatInt(number, 10)
// we only set url for UI purposes so there is no helper function for key
metadata[fmt.Sprintf("pr-url:%s", repoURL)] = url
return metadata
}
// getPullRequestNumberFromMetadata returns the pull request number and URL from the metadata map.
// If no pull request number is found, -1 is returned.
func getPullRequestNumberFromMetadata(metadata map[string]string, repoURL string) int64 {
if metadata == nil {
return -1
}
prNumStr := metadata[pullRequestMetadataKey(repoURL)]
if prNumStr == "" {
return -1
}
intVal, err := strconv.ParseInt(prNumStr, 10, 0)
if err != nil {
return -1
}
return intVal
}