Skip to content

Commit e44caeb

Browse files
committed
Close all BitbucketApi client and reduce the number of client instance in SCMSource when process repositories
1 parent 0af4d1e commit e44caeb

11 files changed

Lines changed: 358 additions & 353 deletions

File tree

docs/USER_GUIDE.adoc

Lines changed: 1 addition & 1 deletion
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

Lines changed: 85 additions & 295 deletions
Large diffs are not rendered by default.

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

Lines changed: 197 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@
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;
2930
import edu.umd.cs.findbugs.annotations.CheckForNull;
3031
import edu.umd.cs.findbugs.annotations.NonNull;
32+
import edu.umd.cs.findbugs.annotations.Nullable;
3133
import hudson.Util;
3234
import hudson.model.TaskListener;
3335
import java.io.Closeable;
@@ -39,16 +41,146 @@
3941
import java.util.Map;
4042
import java.util.Set;
4143
import jenkins.scm.api.SCMHead;
44+
import jenkins.scm.api.SCMHeadObserver;
4245
import jenkins.scm.api.SCMHeadOrigin;
46+
import jenkins.scm.api.SCMRevision;
47+
import jenkins.scm.api.SCMSourceCriteria;
48+
import jenkins.scm.api.SCMSourceCriteria.Probe;
4349
import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy;
4450
import jenkins.scm.api.trait.SCMSourceRequest;
51+
import jenkins.scm.api.trait.SCMSourceRequest.LazyRevisionLambda;
4552

4653
/**
4754
* The {@link SCMSourceRequest} for bitbucket.
4855
*
4956
* @since 2.2.0
5057
*/
5158
public class BitbucketSCMSourceRequest extends SCMSourceRequest {
59+
60+
private class BitbucketProbeFactory<I> implements SCMSourceRequest.ProbeLambda<SCMHead, I> {
61+
private final BitbucketApi client;
62+
63+
public BitbucketProbeFactory(BitbucketApi client) {
64+
this.client = client;
65+
}
66+
67+
@NonNull
68+
@Override
69+
public Probe create(@NonNull final SCMHead head, @CheckForNull final I revisionInfo) throws IOException, InterruptedException {
70+
final String hash = (revisionInfo instanceof BitbucketCommit bbRevision) //
71+
? bbRevision.getHash() //
72+
: (String) revisionInfo;
73+
74+
return new SCMSourceCriteria.Probe() {
75+
private static final long serialVersionUID = 1L;
76+
77+
@Override
78+
public String name() {
79+
return head.getName();
80+
}
81+
82+
@Override
83+
public long lastModified() {
84+
try {
85+
BitbucketCommit commit = null;
86+
if (hash != null) {
87+
commit = (revisionInfo instanceof BitbucketCommit bbRevision) //
88+
? bbRevision //
89+
: client.resolveCommit(hash);
90+
}
91+
92+
if (commit == null) {
93+
listener().getLogger().format("Can not resolve commit by hash [%s] on repository %s/%s%n", //
94+
hash, client.getOwner(), client.getRepositoryName());
95+
return 0;
96+
}
97+
return commit.getDateMillis();
98+
} catch (InterruptedException | IOException e) {
99+
listener().getLogger().format("Can not resolve commit by hash [%s] on repository %s/%s%n", //
100+
hash, client.getOwner(), client.getRepositoryName());
101+
return 0;
102+
}
103+
}
104+
105+
@Override
106+
public boolean exists(@NonNull String path) throws IOException {
107+
if (hash == null) {
108+
listener().getLogger() //
109+
.format("Can not resolve path for hash [%s] on repository %s/%s%n", //
110+
hash, client.getOwner(), client.getRepositoryName());
111+
return false;
112+
}
113+
114+
try {
115+
return client.checkPathExists(hash, path);
116+
} catch (InterruptedException e) {
117+
throw new IOException("Interrupted", e);
118+
}
119+
}
120+
};
121+
}
122+
}
123+
124+
public static class BitbucketRevisionFactory<I> implements SCMSourceRequest.LazyRevisionLambda<SCMHead, SCMRevision, I> {
125+
private final BitbucketApi client;
126+
127+
public BitbucketRevisionFactory(BitbucketApi client) {
128+
this.client = client;
129+
}
130+
131+
@NonNull
132+
@Override
133+
public SCMRevision create(@NonNull SCMHead head, @Nullable I input) throws IOException, InterruptedException {
134+
return create(head, input, null);
135+
}
136+
137+
@NonNull
138+
public SCMRevision create(@NonNull SCMHead head,
139+
@Nullable I sourceInput,
140+
@Nullable I targetInput) throws IOException, InterruptedException {
141+
BitbucketCommit sourceCommit = asCommit(sourceInput);
142+
BitbucketCommit targetCommit = asCommit(targetInput);
143+
144+
SCMRevision revision;
145+
if (head instanceof PullRequestSCMHead prHead) {
146+
SCMHead targetHead = prHead.getTarget();
147+
148+
return new PullRequestSCMRevision( //
149+
prHead, //
150+
new BitbucketGitSCMRevision(targetHead, targetCommit), //
151+
new BitbucketGitSCMRevision(prHead, sourceCommit));
152+
} else {
153+
revision = new BitbucketGitSCMRevision(head, sourceCommit);
154+
}
155+
return revision;
156+
}
157+
158+
private BitbucketCommit asCommit(I input) throws IOException, InterruptedException {
159+
if (input instanceof String value) {
160+
return client.resolveCommit(value);
161+
} else if (input instanceof BitbucketCommit commit) {
162+
return commit;
163+
}
164+
return null;
165+
}
166+
}
167+
168+
private class CriteriaWitness implements SCMSourceRequest.Witness {
169+
@Override
170+
public void record(@NonNull SCMHead scmHead, SCMRevision revision, boolean isMatch) {
171+
if (revision == null) {
172+
listener().getLogger().println(" Skipped");
173+
} else {
174+
if (isMatch) {
175+
listener().getLogger().println(" Met criteria");
176+
} else {
177+
listener().getLogger().println(" Does not meet criteria");
178+
}
179+
180+
}
181+
}
182+
}
183+
52184
/**
53185
* {@code true} if branch details need to be fetched.
54186
*/
@@ -355,9 +487,14 @@ public final void setPullRequests(@CheckForNull Iterable<BitbucketPullRequest> p
355487
* or if the pull request details have not been provided by {@link #setPullRequests(Iterable)} yet.
356488
*
357489
* @return the pull request details (may be empty)
490+
* @throws InterruptedException
491+
* @throws IOException
358492
*/
359493
@NonNull
360-
public final Iterable<BitbucketPullRequest> getPullRequests() {
494+
public final Iterable<BitbucketPullRequest> getPullRequests() throws IOException, InterruptedException {
495+
if (pullRequests == null) {
496+
pullRequests = (Iterable<BitbucketPullRequest>) getBitbucketApiClient().getPullRequests();
497+
}
361498
return Util.fixNull(pullRequests);
362499
}
363500

@@ -399,9 +536,14 @@ public final void setBranches(@CheckForNull Iterable<BitbucketBranch> branches)
399536
* or if the branch details have not been provided by {@link #setBranches(Iterable)} yet.
400537
*
401538
* @return the branch details (may be empty)
539+
* @throws IOException if there was a network communications error.
540+
* @throws InterruptedException if interrupted while waiting on remote communications.
402541
*/
403542
@NonNull
404-
public final Iterable<BitbucketBranch> getBranches() {
543+
public final Iterable<BitbucketBranch> getBranches() throws IOException, InterruptedException {
544+
if (branches == null) {
545+
branches = (Iterable<BitbucketBranch>) getBitbucketApiClient().getBranches();
546+
}
405547
return Util.fixNull(branches);
406548
}
407549

@@ -419,9 +561,14 @@ public final void setTags(@CheckForNull Iterable<BitbucketBranch> tags) {
419561
* or if the tag details have not been provided by {@link #setTags(Iterable)} yet.
420562
*
421563
* @return the tag details (may be empty)
564+
* @throws IOException if there was a network communications error.
565+
* @throws InterruptedException if interrupted while waiting on remote communications.
422566
*/
423567
@NonNull
424-
public final Iterable<BitbucketBranch> getTags() {
568+
public final Iterable<BitbucketBranch> getTags() throws IOException, InterruptedException {
569+
if (tags == null) {
570+
tags = (Iterable<BitbucketBranch>) getBitbucketApiClient().getTags();
571+
}
425572
return Util.fixNull(tags);
426573
}
427574

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

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

Lines changed: 26 additions & 15 deletions
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 5 additions & 7 deletions
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)