/
DefaultBuildChooser.java
299 lines (265 loc) · 13.3 KB
/
DefaultBuildChooser.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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
package hudson.plugins.git.util;
import hudson.Extension;
import hudson.EnvVars;
import hudson.model.TaskListener;
import hudson.plugins.git.*;
import hudson.remoting.VirtualChannel;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.RemoteConfig;
import org.jenkinsci.plugins.gitclient.GitClient;
import org.jenkinsci.plugins.gitclient.RepositoryCallback;
import org.kohsuke.stapler.DataBoundConstructor;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.*;
import static java.util.Collections.emptyList;
public class DefaultBuildChooser extends BuildChooser {
/* Ignore symbolic default branch ref. */
private static final BranchSpec HEAD = new BranchSpec("*/HEAD");
@DataBoundConstructor
public DefaultBuildChooser() {
}
/**
* Determines which Revisions to build.
*
* If only one branch is chosen and only one repository is listed, then
* just attempt to find the latest revision number for the chosen branch.
*
* If multiple branches are selected or the branches include wildcards, then
* use the advanced usecase as defined in the getAdvancedCandidateRevisons
* method.
*
* @throws IOException
* @throws GitException
*/
@Override
public Collection<Revision> getCandidateRevisions(boolean isPollCall, String singleBranch,
GitClient git, TaskListener listener, BuildData data, BuildChooserContext context)
throws GitException, IOException, InterruptedException {
verbose(listener,"getCandidateRevisions({0},{1},,,{2}) considering branches to build",isPollCall,singleBranch,data);
// if the branch name contains more wildcards then the simple usecase
// does not apply and we need to skip to the advanced usecase
if (singleBranch == null || singleBranch.contains("*"))
return getAdvancedCandidateRevisions(isPollCall,listener,new GitUtils(listener,git),data, context);
// check if we're trying to build a specific commit
// this only makes sense for a build, there is no
// reason to poll for a commit
if (!isPollCall && singleBranch.matches("[0-9a-f]{6,40}")) {
try {
ObjectId sha1 = git.revParse(singleBranch);
Revision revision = new Revision(sha1);
revision.getBranches().add(new Branch("detached", sha1));
verbose(listener,"Will build the detached SHA1 {0}",sha1);
return Collections.singletonList(revision);
} catch (GitException e) {
// revision does not exist, may still be a branch
// for example a branch called "badface" would show up here
verbose(listener, "Not a valid SHA1 {0}", singleBranch);
}
}
Collection<Revision> revisions = new ArrayList<Revision>();
// if it doesn't contain '/' then it could be either a tag or an unqualified branch
if (!singleBranch.contains("/")) {
// the 'branch' could actually be a tag:
Set<String> tags = git.getTagNames(singleBranch);
if(tags.size() != 0) {
verbose(listener, "{0} is a tag");
return getHeadRevision(isPollCall, singleBranch, git, listener, data);
}
// <tt>BRANCH</tt> is recognized as a shorthand of <tt>*/BRANCH</tt>
// so check all remotes to fully qualify this branch spec
for (RemoteConfig config : gitSCM.getRepositories()) {
String repository = config.getName();
String fqbn = repository + "/" + singleBranch;
verbose(listener, "Qualifying {0} as a branch in repository {1} -> {2}", singleBranch, repository, fqbn);
revisions.addAll(getHeadRevision(isPollCall, fqbn, git, listener, data));
}
} else {
// either the branch is qualified (first part should match a valid remote)
// or it is still unqualified, but the branch name contains a '/'
List<String> possibleQualifiedBranches = new ArrayList<String>();
boolean singleBranchIsQualified = false;
for (RemoteConfig config : gitSCM.getRepositories()) {
String repository = config.getName();
if (singleBranch.startsWith(repository + "/")) {
singleBranchIsQualified = true;
break;
}
String fqbn = repository + "/" + singleBranch;
verbose(listener, "Qualifying {0} as a branch in repository {1} -> {2}", singleBranch, repository, fqbn);
possibleQualifiedBranches.add(fqbn);
}
if (singleBranchIsQualified) {
revisions.addAll(getHeadRevision(isPollCall, singleBranch, git, listener, data));
} else {
for (String fqbn : possibleQualifiedBranches) {
revisions.addAll(getHeadRevision(isPollCall, fqbn, git, listener, data));
}
}
}
return revisions;
}
private Collection<Revision> getHeadRevision(boolean isPollCall, String singleBranch, GitClient git, TaskListener listener, BuildData data) throws InterruptedException {
try {
ObjectId sha1 = git.revParse(singleBranch);
verbose(listener, "rev-parse {0} -> {1}", singleBranch, sha1);
// if polling for changes don't select something that has
// already been built as a build candidate
if (isPollCall && data.hasBeenBuilt(sha1)) {
verbose(listener, "{0} has already been built", sha1);
return emptyList();
}
verbose(listener, "Found a new commit {0} to be built on {1}", sha1, singleBranch);
Revision revision = new Revision(sha1);
revision.getBranches().add(new Branch(singleBranch, sha1));
return Collections.singletonList(revision);
/*
// calculate the revisions that are new compared to the last build
List<Revision> candidateRevs = new ArrayList<Revision>();
List<ObjectId> allRevs = git.revListAll(); // index 0 contains the newest revision
if (data != null && allRevs != null) {
Revision lastBuiltRev = data.getLastBuiltRevision();
if (lastBuiltRev == null) {
return Collections.singletonList(objectId2Revision(singleBranch, sha1));
}
int indexOfLastBuildRev = allRevs.indexOf(lastBuiltRev.getSha1());
if (indexOfLastBuildRev == -1) {
// mhmmm ... can happen when branches are switched.
return Collections.singletonList(objectId2Revision(singleBranch, sha1));
}
List<ObjectId> newRevisionsSinceLastBuild = allRevs.subList(0, indexOfLastBuildRev);
// translate list of ObjectIds into list of Revisions
for (ObjectId objectId : newRevisionsSinceLastBuild) {
candidateRevs.add(objectId2Revision(singleBranch, objectId));
}
}
if (candidateRevs.isEmpty()) {
return Collections.singletonList(objectId2Revision(singleBranch, sha1));
}
return candidateRevs;
*/
} catch (GitException e) {
// branch does not exist, there is nothing to build
verbose(listener, "Failed to rev-parse: {0}", singleBranch);
return emptyList();
}
}
private Revision objectId2Revision(String singleBranch, ObjectId sha1) {
Revision revision = new Revision(sha1);
revision.getBranches().add(new Branch(singleBranch, sha1));
return revision;
}
/**
* In order to determine which Revisions to build.
*
* Does the following :
* 1. Find all the branch revisions
* 2. Filter out branches that we don't care about from the revisions.
* Any Revisions with no interesting branches are dropped.
* 3. Get rid of any revisions that are wholly subsumed by another
* revision we're considering.
* 4. Get rid of any revisions that we've already built.
* 5. Sort revisions from old to new.
*
* NB: Alternate BuildChooser implementations are possible - this
* may be beneficial if "only 1" branch is to be built, as much of
* this work is irrelevant in that usecase.
* @throws IOException
* @throws GitException
*/
private List<Revision> getAdvancedCandidateRevisions(boolean isPollCall, TaskListener listener, GitUtils utils, BuildData data, BuildChooserContext context) throws GitException, IOException, InterruptedException {
EnvVars env = context.getBuild().getEnvironment();
// 1. Get all the (branch) revisions that exist
List<Revision> revs = new ArrayList<Revision>(utils.getAllBranchRevisions());
verbose(listener, "Starting with all the branches: {0}", revs);
// 2. Filter out any revisions that don't contain any branches that we
// actually care about (spec)
for (Iterator<Revision> i = revs.iterator(); i.hasNext();) {
Revision r = i.next();
// filter out uninteresting branches
for (Iterator<Branch> j = r.getBranches().iterator(); j.hasNext();) {
Branch b = j.next();
boolean keep = false;
for (BranchSpec bspec : gitSCM.getBranches()) {
if (bspec.matches(b.getName(), env)) {
keep = true;
break;
}
}
if (!keep) {
verbose(listener, "Ignoring {0} because it doesn''t match branch specifier", b);
j.remove();
}
}
// filter out HEAD ref if it's not the only ref
if (r.getBranches().size() > 1) {
for (Iterator<Branch> j = r.getBranches().iterator(); j.hasNext();) {
Branch b = j.next();
if (HEAD.matches(b.getName(), env)) {
verbose(listener, "Ignoring {0} because there''s named branch for this revision", b.getName());
j.remove();
}
}
}
if (r.getBranches().size() == 0) {
verbose(listener, "Ignoring {0} because we don''t care about any of the branches that point to it", r);
i.remove();
}
}
verbose(listener, "After branch filtering: {0}", revs);
// 3. We only want 'tip' revisions
revs = utils.filterTipBranches(revs);
verbose(listener, "After non-tip filtering: {0}", revs);
// 4. Finally, remove any revisions that have already been built.
verbose(listener, "Removing what''s already been built: {0}", data.getBuildsByBranchName());
Revision lastBuiltRevision = data.getLastBuiltRevision();
for (Iterator<Revision> i = revs.iterator(); i.hasNext();) {
Revision r = i.next();
if (data.hasBeenBuilt(r.getSha1())) {
i.remove();
// keep track of new branches pointing to the last built revision
if (lastBuiltRevision != null && lastBuiltRevision.getSha1().equals(r.getSha1())) {
lastBuiltRevision = r;
}
}
}
verbose(listener, "After filtering out what''s already been built: {0}", revs);
// if we're trying to run a build (not an SCM poll) and nothing new
// was found then just run the last build again but ensure that the branch list
// is ordered according to the configuration. Sorting the branch list ensures
// a deterministic value for GIT_BRANCH and allows a git-flow style workflow
// with fast-forward merges between branches
if (!isPollCall && revs.isEmpty() && lastBuiltRevision != null) {
verbose(listener, "Nothing seems worth building, so falling back to the previously built revision: {0}", data.getLastBuiltRevision());
return Collections.singletonList(utils.sortBranchesForRevision(lastBuiltRevision, gitSCM.getBranches(), env));
}
// 5. sort them by the date of commit, old to new
// this ensures the fairness in scheduling.
final List<Revision> in = revs;
return utils.git.withRepository(new RepositoryCallback<List<Revision>>() {
public List<Revision> invoke(Repository repo, VirtualChannel channel) throws IOException, InterruptedException {
Collections.sort(in,new CommitTimeComparator(repo));
return in;
}
});
}
/**
* Write the message to the listener only when the verbose mode is on.
*/
private void verbose(TaskListener listener, String format, Object... args) {
if (GitSCM.VERBOSE)
listener.getLogger().println(MessageFormat.format(format,args));
}
@Extension
public static final class DescriptorImpl extends BuildChooserDescriptor {
@Override
public String getDisplayName() {
return Messages.BuildChooser_Default();
}
@Override
public String getLegacyId() {
return "Default";
}
}
}