Skip to content

Commit 02a5660

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

11 files changed

Lines changed: 357 additions & 354 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: 196 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@
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;
33-
import java.io.Closeable;
3435
import java.io.IOException;
3536
import java.util.Collections;
3637
import java.util.EnumSet;
@@ -39,7 +40,11 @@
3940
import java.util.Map;
4041
import java.util.Set;
4142
import jenkins.scm.api.SCMHead;
43+
import jenkins.scm.api.SCMHeadObserver;
4244
import jenkins.scm.api.SCMHeadOrigin;
45+
import jenkins.scm.api.SCMRevision;
46+
import jenkins.scm.api.SCMSourceCriteria;
47+
import jenkins.scm.api.SCMSourceCriteria.Probe;
4348
import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy;
4449
import jenkins.scm.api.trait.SCMSourceRequest;
4550

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

@@ -399,9 +534,14 @@ public final void setBranches(@CheckForNull Iterable<BitbucketBranch> branches)
399534
* or if the branch details have not been provided by {@link #setBranches(Iterable)} yet.
400535
*
401536
* @return the branch details (may be empty)
537+
* @throws IOException if there was a network communications error.
538+
* @throws InterruptedException if interrupted while waiting on remote communications.
402539
*/
403540
@NonNull
404-
public final Iterable<BitbucketBranch> getBranches() {
541+
public final Iterable<BitbucketBranch> getBranches() throws IOException, InterruptedException {
542+
if (branches == null) {
543+
branches = (Iterable<BitbucketBranch>) getBitbucketApiClient().getBranches();
544+
}
405545
return Util.fixNull(branches);
406546
}
407547

@@ -419,9 +559,14 @@ public final void setTags(@CheckForNull Iterable<BitbucketBranch> tags) {
419559
* or if the tag details have not been provided by {@link #setTags(Iterable)} yet.
420560
*
421561
* @return the tag details (may be empty)
562+
* @throws IOException if there was a network communications error.
563+
* @throws InterruptedException if interrupted while waiting on remote communications.
422564
*/
423565
@NonNull
424-
public final Iterable<BitbucketBranch> getTags() {
566+
public final Iterable<BitbucketBranch> getTags() throws IOException, InterruptedException {
567+
if (tags == null) {
568+
tags = (Iterable<BitbucketBranch>) getBitbucketApiClient().getTags();
569+
}
425570
return Util.fixNull(tags);
426571
}
427572

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

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)