-
Notifications
You must be signed in to change notification settings - Fork 606
/
GhprbPullRequestMerge.java
233 lines (187 loc) · 7.71 KB
/
GhprbPullRequestMerge.java
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
226
227
228
229
230
231
232
233
package org.jenkinsci.plugins.ghprb;
import java.io.IOException;
import java.io.PrintStream;
import java.util.concurrent.ConcurrentMap;
import org.kohsuke.github.GHBranch;
import org.kohsuke.github.GHPullRequestCommitDetail.Commit;
import org.kohsuke.github.GHPullRequest;
import org.kohsuke.github.GHPullRequestCommitDetail;
import org.kohsuke.github.GHUser;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import com.google.common.annotations.VisibleForTesting;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.BuildListener;
import hudson.model.Cause;
import hudson.model.Result;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Publisher;
import hudson.tasks.Recorder;
import hudson.util.FormValidation;
public class GhprbPullRequestMerge extends Recorder {
private PrintStream logger;
private final boolean onlyAdminsMerge;
private final boolean disallowOwnCode;
private boolean onlyTriggerPhrase;
private String mergeComment;
@DataBoundConstructor
public GhprbPullRequestMerge(String mergeComment, boolean onlyTriggerPhrase, boolean onlyAdminsMerge, boolean disallowOwnCode) {
this.mergeComment = mergeComment;
this.onlyTriggerPhrase = onlyTriggerPhrase;
this.onlyAdminsMerge = onlyAdminsMerge;
this.disallowOwnCode = disallowOwnCode;
}
public String getMergeComment() {
return mergeComment;
}
public boolean isOnlyTriggerPhrase() {
return onlyTriggerPhrase;
}
public boolean isOnlyAdminsMerge() {
return onlyAdminsMerge;
}
public boolean isDisallowOwnCode() {
return disallowOwnCode;
}
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.BUILD;
}
private GhprbTrigger trigger;
private Ghprb helper;
private GhprbCause cause;
private GHPullRequest pr;
@VisibleForTesting
void setHelper(Ghprb helper) {
this.helper = helper;
}
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, final BuildListener listener) throws InterruptedException, IOException {
logger = listener.getLogger();
AbstractProject<?, ?> project = build.getProject();
if (build.getResult().isWorseThan(Result.SUCCESS)) {
logger.println("Build did not succeed, merge will not be run");
return true;
}
trigger = Ghprb.extractTrigger(project);
if (trigger == null)
return false;
cause = getCause(build);
if (cause == null) {
return true;
}
ConcurrentMap<Integer, GhprbPullRequest> pulls = trigger.getDescriptor().getPullRequests(project.getFullName());
pr = pulls.get(cause.getPullID()).getPullRequest();
if (pr == null) {
logger.println("Pull request is null for ID: " + cause.getPullID());
logger.println("" + pulls.toString());
return false;
}
Boolean isMergeable = cause.isMerged();
if (helper == null) {
helper = new Ghprb(project, trigger, pulls);
helper.init();
}
if (isMergeable == null || !isMergeable) {
logger.println("Pull request cannot be automerged.");
commentOnRequest("Pull request is not mergeable.");
listener.finished(Result.FAILURE);
return false;
}
GHUser triggerSender = cause.getTriggerSender();
// ignore comments from bot user, this fixes an issue where the bot would auto-merge
// a PR when the 'request for testing' phrase contains the PR merge trigger phrase and
// the bot is a member of a whitelisted organisation
if (helper.isBotUser(triggerSender)) {
logger.println("Comment from bot user " + triggerSender.getLogin() + " ignored.");
return false;
}
boolean merge = true;
String commentBody = cause.getCommentBody();
if (isOnlyAdminsMerge() && (triggerSender == null || !helper.isAdmin(triggerSender) )) {
merge = false;
logger.println("Only admins can merge this pull request, " + triggerSender.getLogin() + " is not an admin.");
commentOnRequest(String.format("Code not merged because %s is not in the Admin list.", triggerSender.getName()));
}
if (isOnlyTriggerPhrase() && (commentBody == null || !helper.isTriggerPhrase(cause.getCommentBody()) )) {
merge = false;
logger.println("The comment does not contain the required trigger phrase.");
commentOnRequest(String.format("Please comment with '%s' to automerge this request", trigger.getTriggerPhrase()));
}
if (isDisallowOwnCode() && (triggerSender == null || isOwnCode(pr, triggerSender) )) {
merge = false;
logger.println("The commentor is also one of the contributors.");
commentOnRequest(String.format("Code not merged because %s has committed code in the request.", triggerSender.getName()));
}
if (merge) {
logger.println("Merging the pull request");
pr.merge(getMergeComment());
logger.println("Pull request successfully merged");
// deleteBranch(); //TODO: Update so it also deletes the branch being pulled from. probably make it an option.
}
if (merge) {
listener.finished(Result.SUCCESS);
} else {
listener.finished(Result.FAILURE);
}
return merge;
}
private void deleteBranch() {
String branchName = pr.getHead().getRef();
try {
GHBranch branch = pr.getRepository().getBranches().get(branchName);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void commentOnRequest(String comment) {
try {
helper.getRepository().addComment(pr.getNumber(), comment);
} catch (Exception e) {
logger.println("Failed to add comment");
e.printStackTrace(logger);
}
}
private boolean isOwnCode(GHPullRequest pr, GHUser committer) {
try {
String commentorName = committer.getName();
for (GHPullRequestCommitDetail detail : pr.listCommits()) {
Commit commit = detail.getCommit();
String committerName = commit.getCommitter().getName();
if (committerName.equalsIgnoreCase(commentorName)) {
return true;
}
}
} catch (IOException e) {
logger.println("Unable to get committer name");
e.printStackTrace(logger);
}
return false;
}
private GhprbCause getCause(AbstractBuild<?, ?> build) {
Cause cause = build.getCause(GhprbCause.class);
if (cause == null || (!(cause instanceof GhprbCause)))
return null;
return (GhprbCause) cause;
}
@Extension(ordinal = -1)
public static class DescriptorImpl extends BuildStepDescriptor<Publisher> {
@Override
public String getDisplayName() {
return "Github Pull Request Merger";
}
@Override
public boolean isApplicable(Class<? extends AbstractProject> jobType) {
return true;
}
public FormValidation doCheck(@AncestorInPath AbstractProject<?, ?> project, @QueryParameter String value) throws IOException {
return FilePath.validateFileMask(project.getSomeWorkspace(), value);
}
}
}