Skip to content

Commit 0b31752

Browse files
committed
Close all BitbucketApi client and reduce the number of client instance in SCMSource when process repositories
Do not authenticate requests with host different than serverURL
1 parent 0af4d1e commit 0b31752

18 files changed

+490
-438
lines changed

docs/USER_GUIDE.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ Bitbucket https://community.atlassian.com/t5/Bitbucket-articles/Announcement-Bit
175175

176176
The plugin can make use of an app password instead of the standard username/password.
177177

178-
First, create a new _app password_ in Bitbucket as instructed in the https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/[Bitbucket App Passwords Documentation]. At least allow _read_ access for repositories. Also, you may need to allow _read_ and _write_ access for webhooks depending on your pipeline's triggers.
178+
First, create a new _app password_ in Bitbucket as instructed in the https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/[Bitbucket App Passwords Documentation]. At least allow _read_ access for repositories and pull requests. Also, you may need to allow _read_ and _write_ access for webhooks depending on your pipeline's triggers.
179179

180180
Then create a new _Username with password credentials_ in Jenkins, enter the Bitbucket username (not the email) in the _Username_ field and the created app password in the _Password_ field.
181181

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java

+85-295
Large diffs are not rendered by default.

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceRequest.java

+208-9
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,30 @@
2525

2626
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi;
2727
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBranch;
28+
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketCommit;
2829
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest;
30+
import com.cloudbees.jenkins.plugins.bitbucket.filesystem.BitbucketSCMFile;
2931
import edu.umd.cs.findbugs.annotations.CheckForNull;
3032
import edu.umd.cs.findbugs.annotations.NonNull;
33+
import edu.umd.cs.findbugs.annotations.Nullable;
34+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
3135
import hudson.Util;
3236
import hudson.model.TaskListener;
33-
import java.io.Closeable;
3437
import java.io.IOException;
3538
import java.util.Collections;
3639
import java.util.EnumSet;
3740
import java.util.HashMap;
3841
import java.util.HashSet;
3942
import java.util.Map;
4043
import java.util.Set;
44+
import jenkins.scm.api.SCMFile.Type;
4145
import jenkins.scm.api.SCMHead;
46+
import jenkins.scm.api.SCMHeadObserver;
4247
import jenkins.scm.api.SCMHeadOrigin;
48+
import jenkins.scm.api.SCMProbe;
49+
import jenkins.scm.api.SCMProbeStat;
50+
import jenkins.scm.api.SCMRevision;
51+
import jenkins.scm.api.SCMSourceCriteria.Probe;
4352
import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy;
4453
import jenkins.scm.api.trait.SCMSourceRequest;
4554

@@ -49,6 +58,139 @@
4958
* @since 2.2.0
5059
*/
5160
public class BitbucketSCMSourceRequest extends SCMSourceRequest {
61+
62+
private class BitbucketProbeFactory<I> implements SCMSourceRequest.ProbeLambda<SCMHead, I> {
63+
private transient final BitbucketApi client;
64+
65+
public BitbucketProbeFactory(BitbucketApi client) {
66+
this.client = client;
67+
}
68+
69+
@SuppressFBWarnings("SE_BAD_FIELD")
70+
@SuppressWarnings("serial")
71+
@NonNull
72+
@Override
73+
public Probe create(@NonNull final SCMHead head, @CheckForNull final I revisionInfo) throws IOException, InterruptedException {
74+
final String hash = (revisionInfo instanceof BitbucketCommit bbRevision) //
75+
? bbRevision.getHash() //
76+
: (String) revisionInfo;
77+
78+
return new SCMProbe() {
79+
80+
@Override
81+
public void close() throws IOException {
82+
// client will be closed by BitbucketSCMSourceRequest
83+
}
84+
85+
@Override
86+
public String name() {
87+
return head.getName();
88+
}
89+
90+
@Override
91+
public long lastModified() {
92+
try {
93+
BitbucketCommit commit = null;
94+
if (hash != null) {
95+
commit = (revisionInfo instanceof BitbucketCommit bbRevision) //
96+
? bbRevision //
97+
: client.resolveCommit(hash);
98+
}
99+
100+
if (commit == null) {
101+
listener().getLogger().format("Can not resolve commit by hash [%s] on repository %s/%s%n", //
102+
hash, client.getOwner(), client.getRepositoryName());
103+
return 0;
104+
}
105+
return commit.getDateMillis();
106+
} catch (InterruptedException | IOException e) {
107+
listener().getLogger().format("Can not resolve commit by hash [%s] on repository %s/%s%n", //
108+
hash, client.getOwner(), client.getRepositoryName());
109+
return 0;
110+
}
111+
}
112+
113+
@Override
114+
public SCMProbeStat stat(@NonNull String path) throws IOException {
115+
if (hash == null) {
116+
listener().getLogger() //
117+
.format("Can not resolve path for hash [%s] on repository %s/%s%n", //
118+
hash, client.getOwner(), client.getRepositoryName());
119+
return SCMProbeStat.fromType(Type.NONEXISTENT);
120+
}
121+
122+
try {
123+
Type pathType = new BitbucketSCMFile(client, name(), hash).child(path).getType();
124+
return SCMProbeStat.fromType(pathType);
125+
} catch (InterruptedException e) {
126+
throw new IOException("Interrupted", e);
127+
}
128+
}
129+
};
130+
}
131+
}
132+
133+
public static class BitbucketRevisionFactory<I> implements SCMSourceRequest.LazyRevisionLambda<SCMHead, SCMRevision, I> {
134+
private final BitbucketApi client;
135+
136+
public BitbucketRevisionFactory(BitbucketApi client) {
137+
this.client = client;
138+
}
139+
140+
@NonNull
141+
@Override
142+
public SCMRevision create(@NonNull SCMHead head, @Nullable I input) throws IOException, InterruptedException {
143+
return create(head, input, null);
144+
}
145+
146+
@NonNull
147+
public SCMRevision create(@NonNull SCMHead head,
148+
@Nullable I sourceInput,
149+
@Nullable I targetInput) throws IOException, InterruptedException {
150+
BitbucketCommit sourceCommit = asCommit(sourceInput);
151+
BitbucketCommit targetCommit = asCommit(targetInput);
152+
153+
SCMRevision revision;
154+
if (head instanceof PullRequestSCMHead prHead) {
155+
SCMHead targetHead = prHead.getTarget();
156+
157+
return new PullRequestSCMRevision( //
158+
prHead, //
159+
new BitbucketGitSCMRevision(targetHead, targetCommit), //
160+
new BitbucketGitSCMRevision(prHead, sourceCommit));
161+
} else {
162+
revision = new BitbucketGitSCMRevision(head, sourceCommit);
163+
}
164+
return revision;
165+
}
166+
167+
@Nullable
168+
private BitbucketCommit asCommit(I input) throws IOException, InterruptedException {
169+
if (input instanceof String value) {
170+
return client.resolveCommit(value);
171+
} else if (input instanceof BitbucketCommit commit) {
172+
return commit;
173+
}
174+
return null;
175+
}
176+
}
177+
178+
private class CriteriaWitness implements SCMSourceRequest.Witness {
179+
@Override
180+
public void record(@NonNull SCMHead scmHead, SCMRevision revision, boolean isMatch) { // NOSONAR
181+
if (revision == null) {
182+
listener().getLogger().println(" Skipped");
183+
} else {
184+
if (isMatch) {
185+
listener().getLogger().println(" Met criteria");
186+
} else {
187+
listener().getLogger().println(" Does not meet criteria");
188+
}
189+
190+
}
191+
}
192+
}
193+
52194
/**
53195
* {@code true} if branch details need to be fetched.
54196
*/
@@ -355,9 +497,14 @@ public final void setPullRequests(@CheckForNull Iterable<BitbucketPullRequest> p
355497
* or if the pull request details have not been provided by {@link #setPullRequests(Iterable)} yet.
356498
*
357499
* @return the pull request details (may be empty)
500+
* @throws IOException If the request to retrieve the full details encounters an issue.
501+
* @throws InterruptedException If the request to retrieve the full details is interrupted.
358502
*/
359503
@NonNull
360-
public final Iterable<BitbucketPullRequest> getPullRequests() {
504+
public final Iterable<BitbucketPullRequest> getPullRequests() throws IOException, InterruptedException {
505+
if (pullRequests == null) {
506+
pullRequests = (Iterable<BitbucketPullRequest>) getBitbucketApiClient().getPullRequests();
507+
}
361508
return Util.fixNull(pullRequests);
362509
}
363510

@@ -399,9 +546,14 @@ public final void setBranches(@CheckForNull Iterable<BitbucketBranch> branches)
399546
* or if the branch details have not been provided by {@link #setBranches(Iterable)} yet.
400547
*
401548
* @return the branch details (may be empty)
549+
* @throws IOException if there was a network communications error.
550+
* @throws InterruptedException if interrupted while waiting on remote communications.
402551
*/
403552
@NonNull
404-
public final Iterable<BitbucketBranch> getBranches() {
553+
public final Iterable<BitbucketBranch> getBranches() throws IOException, InterruptedException {
554+
if (branches == null) {
555+
branches = (Iterable<BitbucketBranch>) getBitbucketApiClient().getBranches();
556+
}
405557
return Util.fixNull(branches);
406558
}
407559

@@ -419,9 +571,14 @@ public final void setTags(@CheckForNull Iterable<BitbucketBranch> tags) {
419571
* or if the tag details have not been provided by {@link #setTags(Iterable)} yet.
420572
*
421573
* @return the tag details (may be empty)
574+
* @throws IOException if there was a network communications error.
575+
* @throws InterruptedException if interrupted while waiting on remote communications.
422576
*/
423577
@NonNull
424-
public final Iterable<BitbucketBranch> getTags() {
578+
public final Iterable<BitbucketBranch> getTags() throws IOException, InterruptedException {
579+
if (tags == null) {
580+
tags = (Iterable<BitbucketBranch>) getBitbucketApiClient().getTags();
581+
}
425582
return Util.fixNull(tags);
426583
}
427584

@@ -430,12 +587,54 @@ public final Iterable<BitbucketBranch> getTags() {
430587
*/
431588
@Override
432589
public void close() throws IOException {
433-
if (pullRequests instanceof Closeable closable) {
434-
closable.close();
435-
}
436-
if (branches instanceof Closeable closable) {
437-
closable.close();
590+
if (api != null) {
591+
api.close();
438592
}
439593
super.close();
440594
}
595+
596+
/**
597+
* Processes a head in the context of the current request where an intermediary operation is required before
598+
* the {@link SCMRevision} can be instantiated.
599+
*
600+
* @param head the {@link SCMHead} to process.
601+
* @param intermediateFactory factory method that provides the seed information for both the {@link ProbeLambda}
602+
* and the {@link LazyRevisionLambda}.
603+
* @param <H> the type of {@link SCMHead}.
604+
* @param <I> the type of the intermediary operation result.
605+
* @param <R> the type of {@link SCMRevision}.
606+
* @return {@code true} if the {@link SCMHeadObserver} for this request has completed observing, {@code false} to
607+
* continue processing.
608+
* @throws IOException if there was an I/O error.
609+
* @throws InterruptedException if the processing was interrupted.
610+
*/
611+
public final <H extends SCMHead, I, R extends SCMRevision> boolean process(@NonNull H head,
612+
@CheckForNull IntermediateLambda<I> intermediateFactory)
613+
throws IOException, InterruptedException {
614+
return super.process(head, //
615+
intermediateFactory, //
616+
defaultProbeLamda(), //
617+
defaultRevisionLamda(), //
618+
new CriteriaWitness());
619+
}
620+
621+
@NonNull
622+
<I> ProbeLambda<SCMHead, I> defaultProbeLamda() {
623+
return this.new BitbucketProbeFactory<>(getBitbucketApiClient());
624+
}
625+
626+
@NonNull
627+
<I> ProbeLambda<SCMHead, I> buildProbeLamda(@NonNull BitbucketApi client) {
628+
return this.new BitbucketProbeFactory<>(client);
629+
}
630+
631+
@NonNull
632+
<I> LazyRevisionLambda<SCMHead, SCMRevision, I> defaultRevisionLamda() {
633+
return new BitbucketRevisionFactory<>(getBitbucketApiClient());
634+
}
635+
636+
@NonNull
637+
Witness defaultWitness() {
638+
return this.new CriteriaWitness();
639+
}
441640
}

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait.java

+26-15
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import edu.umd.cs.findbugs.annotations.NonNull;
2929
import hudson.Extension;
3030
import hudson.util.ListBoxModel;
31+
import java.io.IOException;
3132
import jenkins.scm.api.SCMHead;
3233
import jenkins.scm.api.SCMHeadCategory;
3334
import jenkins.scm.api.SCMHeadOrigin;
@@ -242,14 +243,19 @@ public boolean isExcluded(@NonNull SCMSourceRequest request, @NonNull SCMHead he
242243
if (head instanceof BranchSCMHead && request instanceof BitbucketSCMSourceRequest) {
243244
BitbucketSCMSourceRequest req = (BitbucketSCMSourceRequest) request;
244245
String fullName = req.getRepoOwner() + "/" + req.getRepository();
245-
for (BitbucketPullRequest pullRequest : req.getPullRequests()) {
246-
BitbucketRepository source = pullRequest.getSource().getRepository();
247-
if (StringUtils.equalsIgnoreCase(fullName, source.getFullName())
248-
&& pullRequest.getSource().getBranch().getName().equals(head.getName())) {
249-
request.listener().getLogger().println("Discard branch " + head.getName()
250-
+ " because current strategy excludes branches that are also filed as a pull request");
251-
return true;
246+
try {
247+
for (BitbucketPullRequest pullRequest : req.getPullRequests()) {
248+
BitbucketRepository source = pullRequest.getSource().getRepository();
249+
if (StringUtils.equalsIgnoreCase(fullName, source.getFullName())
250+
&& pullRequest.getSource().getBranch().getName().equals(head.getName())) {
251+
request.listener().getLogger().println("Discard branch " + head.getName()
252+
+ " because current strategy excludes branches that are also filed as a pull request");
253+
return true;
254+
}
252255
}
256+
} catch (IOException | InterruptedException e) {
257+
// should never happens because data in the requests has been already initialised
258+
e.printStackTrace(request.listener().getLogger());
253259
}
254260
}
255261
return false;
@@ -268,16 +274,21 @@ public boolean isExcluded(@NonNull SCMSourceRequest request, @NonNull SCMHead he
268274
if (head instanceof BranchSCMHead && request instanceof BitbucketSCMSourceRequest) {
269275
BitbucketSCMSourceRequest req = (BitbucketSCMSourceRequest) request;
270276
String fullName = req.getRepoOwner() + "/" + req.getRepository();
271-
for (BitbucketPullRequest pullRequest : req.getPullRequests()) {
272-
BitbucketRepository source = pullRequest.getSource().getRepository();
273-
if (fullName.equalsIgnoreCase(source.getFullName())
274-
&& pullRequest.getSource().getBranch().getName().equals(head.getName())) {
275-
return false;
277+
try {
278+
for (BitbucketPullRequest pullRequest : req.getPullRequests()) {
279+
BitbucketRepository source = pullRequest.getSource().getRepository();
280+
if (fullName.equalsIgnoreCase(source.getFullName())
281+
&& pullRequest.getSource().getBranch().getName().equals(head.getName())) {
282+
return false;
283+
}
276284
}
285+
request.listener().getLogger().println("Discard branch " + head.getName()
286+
+ " because current strategy excludes branches that are not also filed as a pull request");
287+
return true;
288+
} catch (IOException | InterruptedException e) {
289+
// should never happens because data in the requests has been already initialised
290+
e.printStackTrace(request.listener().getLogger());
277291
}
278-
request.listener().getLogger().println("Discard branch " + head.getName()
279-
+ " because current strategy excludes branches that are not also filed as a pull request");
280-
return true;
281292
}
282293
return false;
283294
}

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java

+3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketException;
3232
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest;
3333
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository;
34+
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRequestException;
3435
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketTeam;
3536
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook;
3637
import com.cloudbees.jenkins.plugins.bitbucket.client.branch.BitbucketCloudBranch;
@@ -668,6 +669,8 @@ public AvatarImage getAvatar(@CheckForNull String url) throws IOException {
668669
return new AvatarImage(avatar, System.currentTimeMillis());
669670
} catch (FileNotFoundException e) {
670671
logger.log(Level.FINE, "Failed to get avatar from URL {0}", url);
672+
} catch (BitbucketRequestException e) {
673+
throw e;
671674
} catch (IOException e) {
672675
throw new IOException("I/O error when parsing response from URL: " + url, e);
673676
}

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/ServerPushEvent.java

+5-7
Original file line numberDiff line numberDiff line change
@@ -263,16 +263,12 @@ private Map<String, BitbucketServerPullRequest> getPullRequests(BitbucketSCMSour
263263
return pullRequests;
264264
}
265265

266-
private Map<String, BitbucketServerPullRequest> loadPullRequests(BitbucketSCMSource src,
267-
NativeServerChange change) throws InterruptedException {
268-
266+
private Map<String, BitbucketServerPullRequest> loadPullRequests(BitbucketSCMSource src, NativeServerChange change) throws InterruptedException {
269267
final BitbucketServerRepository eventRepo = repository;
270-
final BitbucketServerAPIClient api = (BitbucketServerAPIClient) src
271-
.buildBitbucketClient(eventRepo.getOwnerName(), eventRepo.getRepositoryName());
272-
273268
final Map<String, BitbucketServerPullRequest> pullRequests = new HashMap<>();
274269

275-
try {
270+
try (BitbucketServerAPIClient api = (BitbucketServerAPIClient) src
271+
.buildBitbucketClient(eventRepo.getOwnerName(), eventRepo.getRepositoryName())) {
276272
try {
277273
for (final BitbucketServerPullRequest pullRequest : api.getOutgoingOpenPullRequests(change.getRefId())) {
278274
pullRequests.put(pullRequest.getId(), pullRequest);
@@ -294,6 +290,8 @@ private Map<String, BitbucketServerPullRequest> loadPullRequests(BitbucketSCMSou
294290
}
295291
} catch (FileNotFoundException e) {
296292
LOGGER.log(Level.INFO, "No such Repository on Bitbucket: {0}", e.getMessage());
293+
} catch (IOException e1) {
294+
LOGGER.log(Level.INFO, "Comunication fail with server", e1);
297295
}
298296

299297
return pullRequests;

0 commit comments

Comments
 (0)