diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java index 0a2e35ed2..144ce6f5e 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java @@ -1047,7 +1047,8 @@ private void setPrimaryCloneLinks(List links) { links.forEach(link -> { if (StringUtils.startsWithIgnoreCase(link.getName(), "http")) { // Remove the username from URL because it will be set into the GIT_URL variable - // credentials used to clone or for push/pull could be different than this will cause a failure + // credentials used to git clone or push/pull operation could be different than this (for example SSH) + // and will run into a failure // Restore the behaviour before mirror link feature. link.setHref(URLUtils.removeAuthority(link.getHref())); } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerHeadEvent.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/AbstractNativeServerSCMHeadEvent.java similarity index 83% rename from src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerHeadEvent.java rename to src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/AbstractNativeServerSCMHeadEvent.java index 64a97626d..b3340566d 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerHeadEvent.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/AbstractNativeServerSCMHeadEvent.java @@ -27,13 +27,12 @@ import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSourceContext; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; -import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils; import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerRepository; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.scm.SCM; import java.util.Collections; import java.util.Map; -import java.util.logging.Logger; import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMHeadEvent; import jenkins.scm.api.SCMHeadObserver; @@ -41,15 +40,13 @@ import jenkins.scm.api.SCMRevision; import jenkins.scm.api.SCMSource; -abstract class NativeServerHeadEvent

extends SCMHeadEvent

{ - private static final Logger LOGGER = Logger.getLogger(NativeServerHeadEvent.class.getName()); - +abstract class AbstractNativeServerSCMHeadEvent

extends SCMHeadEvent

{ @NonNull - private final String serverUrl; + private final String serverURL; - NativeServerHeadEvent(String serverUrl, Type type, P payload, String origin) { + AbstractNativeServerSCMHeadEvent(String serverURL, Type type, P payload, String origin) { super(type, payload, origin); - this.serverUrl = serverUrl; + this.serverURL = serverURL; } @NonNull @@ -66,7 +63,7 @@ public boolean isMatch(@NonNull SCMNavigator navigator) { final BitbucketSCMNavigator bbNav = (BitbucketSCMNavigator) navigator; - return isServerUrlMatch(bbNav.getServerUrl()) && bbNav.getRepoOwner().equalsIgnoreCase(getRepository().getOwnerName()); + return isServerURLMatch(bbNav.getServerUrl()) && bbNav.getRepoOwner().equalsIgnoreCase(getRepository().getOwnerName()); } @Override @@ -87,12 +84,12 @@ public Map heads(@NonNull SCMSource source) { @NonNull protected abstract Map heads(@NonNull BitbucketSCMSource source); - protected boolean isServerUrlMatch(String serverUrl) { - if (serverUrl == null || BitbucketCloudEndpoint.SERVER_URL.equals(serverUrl)) { + protected boolean isServerURLMatch(String serverURL) { + if (serverURL == null || BitbucketApiUtils.isCloud(serverURL)) { return false; // this is Bitbucket Cloud, which is not handled by this processor } - return serverUrl.equals(this.serverUrl); + return serverURL.equals(this.serverURL); } protected boolean eventMatchesRepo(BitbucketSCMSource source) { @@ -111,7 +108,7 @@ private BitbucketSCMSource getMatchingBitbucketSource(SCMSource source) { } final BitbucketSCMSource src = (BitbucketSCMSource) source; - if (!isServerUrlMatch(src.getServerUrl())) { + if (!isServerURLMatch(src.getServerUrl())) { return null; } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/AbstractSCMHeadEvent.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/AbstractSCMHeadEvent.java new file mode 100644 index 000000000..7ea00aced --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/AbstractSCMHeadEvent.java @@ -0,0 +1,113 @@ +/* + * The MIT License + * + * Copyright (c) 2016, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.cloudbees.jenkins.plugins.bitbucket.hooks; + +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketHref; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; +import com.cloudbees.jenkins.plugins.bitbucket.client.events.BitbucketCloudPullRequestEvent; +import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils; +import com.cloudbees.jenkins.plugins.bitbucket.server.events.BitbucketServerPullRequestEvent; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.scm.SCM; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; +import jenkins.scm.api.SCMHeadEvent; +import jenkins.scm.api.SCMNavigator; +import org.apache.commons.lang.StringUtils; + +abstract class AbstractSCMHeadEvent

extends SCMHeadEvent

{ + + protected AbstractSCMHeadEvent(Type type, P payload, String origin) { + super(type, payload, origin); + } + + @Override + public boolean isMatch(@NonNull SCMNavigator navigator) { + if (!(navigator instanceof BitbucketSCMNavigator)) { + return false; + } + BitbucketSCMNavigator bbNav = (BitbucketSCMNavigator) navigator; + if (!isProjectKeyMatch(bbNav.getProjectKey())) { + return false; + } + + if (!isServerURLMatch(bbNav.getServerUrl())) { + return false; + } + return bbNav.getRepoOwner().equalsIgnoreCase(getRepository().getOwnerName()); + } + + protected abstract BitbucketRepository getRepository(); + + private boolean isProjectKeyMatch(String projectKey) { + if (StringUtils.isBlank(projectKey)) { + return true; + } + BitbucketRepository repository = getRepository(); + if (repository.getProject() != null) { + return projectKey.equals(repository.getProject().getKey()); + } + return true; + } + + protected boolean isServerURLMatch(String serverURL) { + if (serverURL == null || BitbucketApiUtils.isCloud(serverURL)) { + // this is a Bitbucket cloud navigator + if (getPayload() instanceof BitbucketServerPullRequestEvent) { + return false; + } + } else { + // this is a Bitbucket server navigator + if (getPayload() instanceof BitbucketCloudPullRequestEvent) { + return false; + } + Map> links = getRepository().getLinks(); + if (links != null && links.containsKey("self")) { + boolean matches = false; + for (BitbucketHref link: links.get("self")) { + try { + URI navUri = new URI(serverURL); + URI evtUri = new URI(link.getHref()); + if (navUri.getHost().equalsIgnoreCase(evtUri.getHost())) { + matches = true; + break; + } + } catch (URISyntaxException e) { + // ignore + } + } + return matches; + } + } + return true; + } + + @Override + public boolean isMatch(@NonNull SCM scm) { + return false; + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/BitbucketSCMSourcePushHookReceiver.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/BitbucketSCMSourcePushHookReceiver.java index 254325bea..333e46eaa 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/BitbucketSCMSourcePushHookReceiver.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/BitbucketSCMSourcePushHookReceiver.java @@ -102,14 +102,20 @@ public HttpResponse doNotify(StaplerRequest2 req) throws IOException { LOGGER.log(Level.FINE, "X-Bitbucket-Type header / server_url request parameter not found. Bitbucket Cloud webhook incoming."); } + HookProcessor hookProcessor = getHookProcessor(type); try { - type.getProcessor().process(type, body, instanceType, origin, serverUrl); + hookProcessor.process(type, body, instanceType, origin, serverUrl); } catch (AbstractMethodError e) { - type.getProcessor().process(body, instanceType); + hookProcessor.process(body, instanceType); } return HttpResponses.ok(); } + /* For test purpose */ + HookProcessor getHookProcessor(HookEventType type) { + return type.getProcessor(); + } + @Override public String getIconFileName() { return null; diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/HookEventType.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/HookEventType.java index 734781947..926dfff42 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/HookEventType.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/HookEventType.java @@ -126,7 +126,7 @@ public enum HookEventType { /** * Sent when hitting the {@literal "Test connection"} button in Bitbucket Server. Apparently undocumented. */ - SERVER_PING("diagnostics:ping", PingHookProcessor.class); + SERVER_PING("diagnostics:ping", NativeServerPingHookProcessor.class); private final String key; @@ -149,8 +149,8 @@ public static HookEventType fromString(String key) { public HookProcessor getProcessor() { try { - return (HookProcessor) clazz.newInstance(); - } catch (InstantiationException | IllegalAccessException e) { + return (HookProcessor) clazz.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException e) { throw new AssertionError("Can not instantiate hook payload processor: " + e.getMessage()); } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/HookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/HookProcessor.java index 626d5e95d..7ef530671 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/HookProcessor.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/HookProcessor.java @@ -90,14 +90,15 @@ public void process(HookEventType type, String payload, BitbucketType instanceTy * @param repository the repository name as configured in the SCMSource */ protected void scmSourceReIndex(final String owner, final String repository) { - try (ACLContext context = ACL.as(ACL.SYSTEM)) { + try (ACLContext context = ACL.as2(ACL.SYSTEM2)) { boolean reindexed = false; for (SCMSourceOwner scmOwner : SCMSourceOwners.all()) { List sources = scmOwner.getSCMSources(); for (SCMSource source : sources) { // Search for the correct SCM source - if (source instanceof BitbucketSCMSource && ((BitbucketSCMSource) source).getRepoOwner().equalsIgnoreCase(owner) - && ((BitbucketSCMSource) source).getRepository().equals(repository)) { + if (source instanceof BitbucketSCMSource scmSource + && scmSource.getRepoOwner().equalsIgnoreCase(owner) + && scmSource.getRepository().equals(repository)) { LOGGER.log(Level.INFO, "Multibranch project found, reindexing " + scmOwner.getName()); scmOwner.onSCMSourceUpdated(source); reindexed = true; diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PingHookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPingHookProcessor.java similarity index 80% rename from src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PingHookProcessor.java rename to src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPingHookProcessor.java index b43426967..45c5196ef 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PingHookProcessor.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPingHookProcessor.java @@ -23,12 +23,17 @@ */ package com.cloudbees.jenkins.plugins.bitbucket.hooks; +import hudson.RestrictedSince; import java.util.logging.Level; import java.util.logging.Logger; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; -public class PingHookProcessor extends HookProcessor { +@Restricted(NoExternalUse.class) +@RestrictedSince("933.3.0") +public class NativeServerPingHookProcessor extends HookProcessor { - private static final Logger LOGGER = Logger.getLogger(PingHookProcessor.class.getName()); + private static final Logger LOGGER = Logger.getLogger(NativeServerPingHookProcessor.class.getName()); @Override public void process(HookEventType hookEvent, String payload, BitbucketType instanceType, String origin) { diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPullRequestHookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPullRequestHookProcessor.java index c118590ec..b9519f313 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPullRequestHookProcessor.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPullRequestHookProcessor.java @@ -24,32 +24,20 @@ package com.cloudbees.jenkins.plugins.bitbucket.hooks; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; -import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSourceContext; -import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMHead; -import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMRevision; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.JsonParser; -import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerRepository; import com.cloudbees.jenkins.plugins.bitbucket.server.events.NativeServerPullRequestEvent; -import com.google.common.base.Ascii; -import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.RestrictedSince; import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; -import jenkins.plugins.git.AbstractGitSCMSource; import jenkins.scm.api.SCMEvent; -import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMHeadEvent; -import jenkins.scm.api.SCMHeadOrigin; -import jenkins.scm.api.SCMRevision; -import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +@Restricted(NoExternalUse.class) +@RestrictedSince("933.3.0") public class NativeServerPullRequestHookProcessor extends HookProcessor { private static final Logger LOGGER = Logger.getLogger(NativeServerPullRequestHookProcessor.class.getName()); @@ -75,14 +63,14 @@ public void process(HookEventType hookEvent, String payload, BitbucketType insta case SERVER_PULL_REQUEST_OPENED: eventType = SCMEvent.Type.CREATED; break; - case SERVER_PULL_REQUEST_MERGED: - case SERVER_PULL_REQUEST_DECLINED: - case SERVER_PULL_REQUEST_DELETED: + case SERVER_PULL_REQUEST_MERGED, + SERVER_PULL_REQUEST_DECLINED, + SERVER_PULL_REQUEST_DELETED: eventType = SCMEvent.Type.REMOVED; break; - case SERVER_PULL_REQUEST_MODIFIED: - case SERVER_PULL_REQUEST_REVIEWER_UPDATED: - case SERVER_PULL_REQUEST_FROM_REF_UPDATED: + case SERVER_PULL_REQUEST_MODIFIED, + SERVER_PULL_REQUEST_REVIEWER_UPDATED, + SERVER_PULL_REQUEST_FROM_REF_UPDATED: eventType = SCMEvent.Type.UPDATED; break; default: @@ -90,82 +78,6 @@ public void process(HookEventType hookEvent, String payload, BitbucketType insta return; } - SCMHeadEvent.fireLater(new HeadEvent(serverUrl, eventType, pullRequestEvent, origin), BitbucketSCMSource.getEventDelaySeconds(), TimeUnit.SECONDS); - } - - private static final class HeadEvent extends NativeServerHeadEvent implements HasPullRequests { - private HeadEvent(String serverUrl, Type type, NativeServerPullRequestEvent payload, String origin) { - super(serverUrl, type, payload, origin); - } - - @Override - protected BitbucketServerRepository getRepository() { - return getPayload().getPullRequest().getDestination().getRepository(); - } - - @NonNull - @Override - protected Map heads(@NonNull BitbucketSCMSource source) { - if (!eventMatchesRepo(source)) { - return Collections.emptyMap(); - } - - final BitbucketSCMSourceContext ctx = contextOf(source); - if (!ctx.wantPRs()) { - return Collections.emptyMap(); // doesn't want PRs, nothing to do here - } - - final BitbucketPullRequest pullRequest = getPayload().getPullRequest(); - final BitbucketRepository sourceRepo = pullRequest.getSource().getRepository(); - final SCMHeadOrigin headOrigin = source.originOf(sourceRepo.getOwnerName(), sourceRepo.getRepositoryName()); - final Set strategies = headOrigin == SCMHeadOrigin.DEFAULT - ? ctx.originPRStrategies() - : ctx.forkPRStrategies(); - final Map result = new HashMap<>(strategies.size()); - for (final ChangeRequestCheckoutStrategy strategy : strategies) { - final String originalBranchName = pullRequest.getSource().getBranch().getName(); - final String branchName = String.format("PR-%s%s", pullRequest.getId(), - strategies.size() > 1 ? "-" + Ascii.toLowerCase(strategy.name()) : ""); - final PullRequestSCMHead head = new PullRequestSCMHead( - branchName, - sourceRepo.getOwnerName(), - sourceRepo.getRepositoryName(), - originalBranchName, - pullRequest, - headOrigin, - strategy - ); - - switch (getType()) { - case CREATED: - case UPDATED: - final String targetHash = pullRequest.getDestination().getCommit().getHash(); - final String pullHash = pullRequest.getSource().getCommit().getHash(); - result.put(head, - new PullRequestSCMRevision(head, - new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), targetHash), - new AbstractGitSCMSource.SCMRevisionImpl(head, pullHash))); - break; - - case REMOVED: - // special case for repo being deleted - result.put(head, null); - break; - - default: - break; - } - } - - return result; - } - - @Override - public Iterable getPullRequests(BitbucketSCMSource src) throws InterruptedException { - if (Type.REMOVED.equals(getType())) { - return Collections.emptySet(); - } - return Collections.singleton(getPayload().getPullRequest()); - } + SCMHeadEvent.fireLater(new ServerHeadEvent(serverUrl, eventType, pullRequestEvent, origin), BitbucketSCMSource.getEventDelaySeconds(), TimeUnit.SECONDS); } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPushHookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPushHookProcessor.java index 87c9478b8..3cbba9b63 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPushHookProcessor.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPushHookProcessor.java @@ -24,46 +24,26 @@ package com.cloudbees.jenkins.plugins.bitbucket.hooks; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; -import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSourceContext; -import com.cloudbees.jenkins.plugins.bitbucket.BitbucketTagSCMHead; -import com.cloudbees.jenkins.plugins.bitbucket.BranchSCMHead; -import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMHead; -import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMRevision; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.JsonParser; -import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerAPIClient; -import com.cloudbees.jenkins.plugins.bitbucket.server.client.pullrequest.BitbucketServerPullRequest; import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerRepository; import com.cloudbees.jenkins.plugins.bitbucket.server.events.NativeServerChange; import com.cloudbees.jenkins.plugins.bitbucket.server.events.NativeServerMirrorRepoSynchronizedEvent; import com.cloudbees.jenkins.plugins.bitbucket.server.events.NativeServerRefsChangedEvent; -import com.google.common.base.Ascii; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; -import edu.umd.cs.findbugs.annotations.CheckForNull; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.io.FileNotFoundException; +import hudson.RestrictedSince; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; -import jenkins.plugins.git.AbstractGitSCMSource; import jenkins.scm.api.SCMEvent; -import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMHeadEvent; -import jenkins.scm.api.SCMHeadOrigin; -import jenkins.scm.api.SCMRevision; -import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; - -import static java.util.Objects.requireNonNull; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +@Restricted(NoExternalUse.class) +@RestrictedSince("933.3.0") public class NativeServerPushHookProcessor extends HookProcessor { private static final Logger LOGGER = Logger.getLogger(NativeServerPushHookProcessor.class.getName()); @@ -74,8 +54,7 @@ public void process(HookEventType hookEvent, String payload, BitbucketType insta } @Override - public void process(HookEventType hookEvent, String payload, BitbucketType instanceType, String origin, - String serverUrl) { + public void process(HookEventType hookEvent, String payload, BitbucketType instanceType, String origin, String serverUrl) { if (payload == null) { return; } @@ -126,228 +105,9 @@ public void process(HookEventType hookEvent, String payload, BitbucketType insta } for (final SCMEvent.Type type : events.keySet()) { - HeadEvent headEvent = new HeadEvent(serverUrl, type, events.get(type), origin, repository, mirrorId); + ServerPushEvent headEvent = new ServerPushEvent(serverUrl, type, events.get(type), origin, repository, mirrorId); SCMHeadEvent.fireLater(headEvent, BitbucketSCMSource.getEventDelaySeconds(), TimeUnit.SECONDS); } } - private static final class HeadEvent extends NativeServerHeadEvent> implements HasPullRequests { - private final BitbucketServerRepository repository; - private final Map> cachedPullRequests = new HashMap<>(); - private final String mirrorId; - - HeadEvent(String serverUrl, Type type, Collection payload, String origin, - BitbucketServerRepository repository, String mirrorId) { - super(serverUrl, type, payload, origin); - this.repository = repository; - this.mirrorId = mirrorId; - } - - @Override - protected BitbucketServerRepository getRepository() { - return repository; - } - - @Override - protected Map heads(BitbucketSCMSource source) { - final Map result = new HashMap<>(); - if (!eventMatchesRepo(source)) { - return result; - } - - addBranchesAndTags(source, result); - try { - addPullRequests(source, result); - } catch (InterruptedException interrupted) { - LOGGER.log(Level.INFO, "Interrupted while fetching Pull Requests from Bitbucket, results may be incomplete."); - } - return result; - } - - private void addBranchesAndTags(BitbucketSCMSource src, Map result) { - for (final NativeServerChange change : getPayload()) { - String refType = change.getRef().getType(); - - if ("BRANCH".equals(refType)) { - final BranchSCMHead head = new BranchSCMHead(change.getRef().getDisplayId()); - final SCMRevision revision = getType() == SCMEvent.Type.REMOVED ? null - : new AbstractGitSCMSource.SCMRevisionImpl(head, change.getToHash()); - result.put(head, revision); - } else if ("TAG".equals(refType)) { - SCMHead head = new BitbucketTagSCMHead(change.getRef().getDisplayId(), 0); - final SCMRevision revision = getType() == SCMEvent.Type.REMOVED ? null - : new AbstractGitSCMSource.SCMRevisionImpl(head, change.getToHash()); - result.put(head, revision); - } else { - LOGGER.log(Level.INFO, "Received event for unknown ref type {0} of ref {1}", - new Object[] { change.getRef().getType(), change.getRef().getDisplayId() }); - } - } - } - - private void addPullRequests(BitbucketSCMSource src, Map result) throws InterruptedException { - if (getType() != SCMEvent.Type.UPDATED) { - return; // adds/deletes won't be handled here - } - - final BitbucketSCMSourceContext ctx = contextOf(src); - if (!ctx.wantPRs()) { - // doesn't want PRs, let the push event handle origin branches - return; - } - - final String sourceOwnerName = src.getRepoOwner(); - final String sourceRepoName = src.getRepository(); - final BitbucketServerRepository eventRepo = repository; - final SCMHeadOrigin headOrigin = src.originOf(eventRepo.getOwnerName(), eventRepo.getRepositoryName()); - final Set strategies = headOrigin == SCMHeadOrigin.DEFAULT - ? ctx.originPRStrategies() : ctx.forkPRStrategies(); - - for (final NativeServerChange change : getPayload()) { - if (!"BRANCH".equals(change.getRef().getType())) { - LOGGER.log(Level.INFO, "Received event for unknown ref type {0} of ref {1}", - new Object[] { change.getRef().getType(), change.getRef().getDisplayId() }); - continue; - } - - // iterate over all PRs in which this change is involved - for (final BitbucketServerPullRequest pullRequest : getPullRequests(src, change).values()) { - final BitbucketServerRepository targetRepo = pullRequest.getDestination().getRepository(); - // check if the target of the PR is actually this source - if (!sourceOwnerName.equalsIgnoreCase(targetRepo.getOwnerName()) - || !sourceRepoName.equalsIgnoreCase(targetRepo.getRepositoryName())) { - continue; - } - - for (final ChangeRequestCheckoutStrategy strategy : strategies) { - if (strategy != ChangeRequestCheckoutStrategy.MERGE && !change.getRefId().equals(pullRequest.getSource().getRefId())) { - continue; // Skip non-merge builds if the changed ref is not the source of the PR. - } - - final String originalBranchName = pullRequest.getSource().getBranch().getName(); - final String branchName = String.format("PR-%s%s", pullRequest.getId(), - strategies.size() > 1 ? "-" + Ascii.toLowerCase(strategy.name()) : ""); - - final BitbucketServerRepository pullRequestRepository = pullRequest.getSource().getRepository(); - final PullRequestSCMHead head = new PullRequestSCMHead( - branchName, - pullRequestRepository.getOwnerName(), - pullRequestRepository.getRepositoryName(), - originalBranchName, - pullRequest, - headOrigin, - strategy - ); - - final String targetHash = pullRequest.getDestination().getCommit().getHash(); - final String pullHash = pullRequest.getSource().getCommit().getHash(); - - result.put(head, - new PullRequestSCMRevision(head, - new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), targetHash), - new AbstractGitSCMSource.SCMRevisionImpl(head, pullHash))); - } - } - } - } - - private Map getPullRequests(BitbucketSCMSource src, NativeServerChange change) - throws InterruptedException { - - Map pullRequests; - final CacheKey cacheKey = new CacheKey(src, change); - synchronized (cachedPullRequests) { - pullRequests = cachedPullRequests.get(cacheKey); - if (pullRequests == null) { - cachedPullRequests.put(cacheKey, pullRequests = loadPullRequests(src, change)); - } - } - - return pullRequests; - } - - private Map loadPullRequests(BitbucketSCMSource src, - NativeServerChange change) throws InterruptedException { - - final BitbucketServerRepository eventRepo = repository; - final BitbucketServerAPIClient api = (BitbucketServerAPIClient) src - .buildBitbucketClient(eventRepo.getOwnerName(), eventRepo.getRepositoryName()); - - final Map pullRequests = new HashMap<>(); - - try { - try { - for (final BitbucketServerPullRequest pullRequest : api.getOutgoingOpenPullRequests(change.getRefId())) { - pullRequests.put(pullRequest.getId(), pullRequest); - } - } catch (final FileNotFoundException e) { - throw e; - } catch (IOException | RuntimeException e) { - LOGGER.log(Level.WARNING, "Failed to retrieve outgoing Pull Requests from Bitbucket", e); - } - - try { - for (final BitbucketServerPullRequest pullRequest : api.getIncomingOpenPullRequests(change.getRefId())) { - pullRequests.put(pullRequest.getId(), pullRequest); - } - } catch (final FileNotFoundException e) { - throw e; - } catch (IOException | RuntimeException e) { - LOGGER.log(Level.WARNING, "Failed to retrieve incoming Pull Requests from Bitbucket", e); - } - } catch (FileNotFoundException e) { - LOGGER.log(Level.INFO, "No such Repository on Bitbucket: {0}", e.getMessage()); - } - - return pullRequests; - } - - @Override - public Collection getPullRequests(BitbucketSCMSource src) throws InterruptedException { - List prs = new ArrayList<>(); - for (final NativeServerChange change : getPayload()) { - Map prsForChange = getPullRequests(src, change); - prs.addAll(prsForChange.values()); - } - - return prs; - } - - @Override - protected boolean eventMatchesRepo(BitbucketSCMSource source) { - return Objects.equals(source.getMirrorId(), this.mirrorId) && super.eventMatchesRepo(source); - } - - } - - private static final class CacheKey { - @NonNull - private final String refId; - @CheckForNull - private final String credentialsId; - - CacheKey(BitbucketSCMSource src, NativeServerChange change) { - this.refId = requireNonNull(change.getRefId()); - this.credentialsId = src.getCredentialsId(); - } - - @Override - public int hashCode() { - return Objects.hash(credentialsId, refId); - } - - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - - if (obj instanceof CacheKey) { - CacheKey other = (CacheKey) obj; - return Objects.equals(credentialsId, other.credentialsId) && refId.equals(other.refId); - } - - return false; - } - } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PREvent.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PREvent.java new file mode 100644 index 000000000..82756c90e --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PREvent.java @@ -0,0 +1,143 @@ +/* + * The MIT License + * + * Copyright (c) 2016, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.cloudbees.jenkins.plugins.bitbucket.hooks; + +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSourceContext; +import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMHead; +import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMRevision; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequestEvent; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import jenkins.plugins.git.AbstractGitSCMSource; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMHeadObserver; +import jenkins.scm.api.SCMHeadOrigin; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; + +import static com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType.PULL_REQUEST_DECLINED; +import static com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType.PULL_REQUEST_MERGED; + +final class PREvent extends AbstractSCMHeadEvent implements HasPullRequests { + private final HookEventType hookEvent; + + PREvent(Type type, BitbucketPullRequestEvent payload, + String origin, + HookEventType hookEvent) { + super(type, payload, origin); + this.hookEvent = hookEvent; + } + + @Override + protected BitbucketRepository getRepository() { + return getPayload().getRepository(); + } + + @NonNull + @Override + public String getSourceName() { + return getRepository().getRepositoryName(); + } + + @NonNull + @Override + public Map heads(@NonNull SCMSource source) { + if (!(source instanceof BitbucketSCMSource)) { + return Collections.emptyMap(); + } + BitbucketSCMSource src = (BitbucketSCMSource) source; + if (!isServerURLMatch(src.getServerUrl())) { + return Collections.emptyMap(); + } + BitbucketRepository repository = getRepository(); + if (!src.getRepoOwner().equalsIgnoreCase(repository.getOwnerName())) { + return Collections.emptyMap(); + } + if (!src.getRepository().equalsIgnoreCase(repository.getRepositoryName())) { + return Collections.emptyMap(); + } + + BitbucketSCMSourceContext ctx = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()) + .withTraits(src.getTraits()); + if (!ctx.wantPRs()) { + // doesn't want PRs, let the push event handle origin branches + return Collections.emptyMap(); + } + BitbucketPullRequest pull = getPayload().getPullRequest(); + String pullRepoOwner = pull.getSource().getRepository().getOwnerName(); + String pullRepository = pull.getSource().getRepository().getRepositoryName(); + SCMHeadOrigin headOrigin = src.originOf(pullRepoOwner, pullRepository); + Set strategies = + headOrigin == SCMHeadOrigin.DEFAULT + ? ctx.originPRStrategies() + : ctx.forkPRStrategies(); + Map result = new HashMap<>(strategies.size()); + for (ChangeRequestCheckoutStrategy strategy : strategies) { + String branchName = "PR-" + pull.getId(); + if (strategies.size() > 1) { + branchName = branchName + "-" + strategy.name().toLowerCase(Locale.ENGLISH); + } + String originalBranchName = pull.getSource().getBranch().getName(); + PullRequestSCMHead head = new PullRequestSCMHead( + branchName, + pullRepoOwner, + pullRepository, + originalBranchName, + pull, + headOrigin, + strategy + ); + if (hookEvent == PULL_REQUEST_DECLINED || hookEvent == PULL_REQUEST_MERGED) { + // special case for repo being deleted + result.put(head, null); + } else { + String targetHash = pull.getDestination().getCommit().getHash(); + String pullHash = pull.getSource().getCommit().getHash(); + + SCMRevision revision = new PullRequestSCMRevision(head, + new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), targetHash), + new AbstractGitSCMSource.SCMRevisionImpl(head, pullHash) + ); + result.put(head, revision); + } + } + return result; + } + + @Override + public Iterable getPullRequests(BitbucketSCMSource src) throws InterruptedException { + if (hookEvent == PULL_REQUEST_DECLINED || hookEvent == PULL_REQUEST_MERGED) { + return Collections.emptyList(); + } + return Collections.singleton(getPayload().getPullRequest()); + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PullRequestHookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PullRequestHookProcessor.java index af0ab537f..3a9c1e32f 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PullRequestHookProcessor.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PullRequestHookProcessor.java @@ -23,56 +23,27 @@ */ package com.cloudbees.jenkins.plugins.bitbucket.hooks; -import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; -import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSourceContext; -import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMHead; -import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMRevision; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketHref; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequestEvent; import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudWebhookPayload; -import com.cloudbees.jenkins.plugins.bitbucket.client.events.BitbucketCloudPullRequestEvent; -import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerWebhookPayload; -import com.cloudbees.jenkins.plugins.bitbucket.server.events.BitbucketServerPullRequestEvent; -import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import hudson.scm.SCM; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; +import hudson.RestrictedSince; import java.util.concurrent.TimeUnit; -import java.util.logging.Logger; -import jenkins.plugins.git.AbstractGitSCMSource; import jenkins.scm.api.SCMEvent; -import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMHeadEvent; -import jenkins.scm.api.SCMHeadObserver; -import jenkins.scm.api.SCMHeadOrigin; -import jenkins.scm.api.SCMNavigator; -import jenkins.scm.api.SCMRevision; -import jenkins.scm.api.SCMSource; -import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; -import org.apache.commons.lang.StringUtils; - -import static com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType.PULL_REQUEST_DECLINED; -import static com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType.PULL_REQUEST_MERGED; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +@Restricted(NoExternalUse.class) +@RestrictedSince("933.3.0") public class PullRequestHookProcessor extends HookProcessor { - private static final Logger LOGGER = Logger.getLogger(PullRequestHookProcessor.class.getName()); - @Override public void process(final HookEventType hookEvent, String payload, final BitbucketType instanceType, String origin) { if (payload != null) { BitbucketPullRequestEvent pull; if (instanceType == BitbucketType.SERVER) { + // plugin webhook case pull = BitbucketServerWebhookPayload.pullRequestEventFromPayload(payload); } else { pull = BitbucketCloudWebhookPayload.pullRequestEventFromPayload(payload); @@ -83,8 +54,8 @@ public void process(final HookEventType hookEvent, String payload, final Bitbuck case PULL_REQUEST_CREATED: eventType = SCMEvent.Type.CREATED; break; - case PULL_REQUEST_DECLINED: - case PULL_REQUEST_MERGED: + case PULL_REQUEST_DECLINED, + PULL_REQUEST_MERGED: eventType = SCMEvent.Type.REMOVED; break; default: @@ -92,163 +63,13 @@ public void process(final HookEventType hookEvent, String payload, final Bitbuck break; } // assume updated as a catch-all type - SCMHeadEvent.fireLater(new HeadEvent(eventType, pull, origin, hookEvent, instanceType), BitbucketSCMSource.getEventDelaySeconds(), TimeUnit.SECONDS); + notifyEvent(new PREvent(eventType, pull, origin, hookEvent), BitbucketSCMSource.getEventDelaySeconds()); } } } - private static final class HeadEvent extends SCMHeadEvent implements HasPullRequests{ - private final HookEventType hookEvent; - private final BitbucketType instanceType; - - private HeadEvent(Type type, BitbucketPullRequestEvent payload, String origin, HookEventType hookEvent, - BitbucketType instanceType) { - super(type, payload, origin); - this.hookEvent = hookEvent; - this.instanceType = instanceType; - } - - @Override - public boolean isMatch(@NonNull SCMNavigator navigator) { - if (!(navigator instanceof BitbucketSCMNavigator)) { - return false; - } - BitbucketSCMNavigator bbNav = (BitbucketSCMNavigator) navigator; - if (!isProjectKeyMatch(bbNav.getProjectKey())) { - return false; - } - - if (!isServerUrlMatch(bbNav.getServerUrl())) { - return false; - } - return bbNav.getRepoOwner().equalsIgnoreCase(getPayload().getRepository().getOwnerName()); - } - - private boolean isProjectKeyMatch(String projectKey) { - if (StringUtils.isBlank(projectKey)) { - return true; - } - if (this.getPayload().getRepository().getProject() != null) { - return projectKey.equals(this.getPayload().getRepository().getProject().getKey()); - } - return true; - } - - private boolean isServerUrlMatch(String serverUrl) { - if (serverUrl == null || BitbucketCloudEndpoint.SERVER_URL.equals(serverUrl)) { - // this is a Bitbucket cloud navigator - if (getPayload() instanceof BitbucketServerPullRequestEvent) { - return false; - } - } else { - // this is a Bitbucket server navigator - if (getPayload() instanceof BitbucketCloudPullRequestEvent) { - return false; - } - Map> links = getPayload().getRepository().getLinks(); - if (links != null && links.containsKey("self")) { - boolean matches = false; - for (BitbucketHref link: links.get("self")) { - try { - URI navUri = new URI(serverUrl); - URI evtUri = new URI(link.getHref()); - if (navUri.getHost().equalsIgnoreCase(evtUri.getHost())) { - matches = true; - break; - } - } catch (URISyntaxException e) { - // ignore - } - } - return matches; - } - } - return true; - } - - @NonNull - @Override - public String getSourceName() { - return getPayload().getRepository().getRepositoryName(); - } - - @NonNull - @Override - @SuppressFBWarnings(value = "SBSC_USE_STRINGBUFFER_CONCATENATION", justification = "TODO needs triage") - public Map heads(@NonNull SCMSource source) { - if (!(source instanceof BitbucketSCMSource)) { - return Collections.emptyMap(); - } - BitbucketSCMSource src = (BitbucketSCMSource) source; - if (!isServerUrlMatch(src.getServerUrl())) { - return Collections.emptyMap(); - } - if (!src.getRepoOwner().equalsIgnoreCase(getPayload().getRepository().getOwnerName())) { - return Collections.emptyMap(); - } - if (!src.getRepository().equalsIgnoreCase(getPayload().getRepository().getRepositoryName())) { - return Collections.emptyMap(); - } - - BitbucketSCMSourceContext ctx = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()) - .withTraits(src.getTraits()); - if (!ctx.wantPRs()) { - // doesn't want PRs, let the push event handle origin branches - return Collections.emptyMap(); - } - BitbucketPullRequest pull = getPayload().getPullRequest(); - String pullRepoOwner = pull.getSource().getRepository().getOwnerName(); - String pullRepository = pull.getSource().getRepository().getRepositoryName(); - SCMHeadOrigin headOrigin = src.originOf(pullRepoOwner, pullRepository); - Set strategies = - headOrigin == SCMHeadOrigin.DEFAULT - ? ctx.originPRStrategies() - : ctx.forkPRStrategies(); - Map result = new HashMap<>(strategies.size()); - for (ChangeRequestCheckoutStrategy strategy : strategies) { - String branchName = "PR-" + pull.getId(); - if (strategies.size() > 1) { - branchName = branchName + "-" + strategy.name().toLowerCase(Locale.ENGLISH); - } - String originalBranchName = pull.getSource().getBranch().getName(); - PullRequestSCMHead head = new PullRequestSCMHead( - branchName, - pullRepoOwner, - pullRepository, - originalBranchName, - pull, - headOrigin, - strategy - ); - if (hookEvent == PULL_REQUEST_DECLINED || hookEvent == PULL_REQUEST_MERGED) { - // special case for repo being deleted - result.put(head, null); - } else { - String targetHash = pull.getDestination().getCommit().getHash(); - String pullHash = pull.getSource().getCommit().getHash(); - - SCMRevision revision = new PullRequestSCMRevision(head, - new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), targetHash), - new AbstractGitSCMSource.SCMRevisionImpl(head, pullHash) - ); - result.put(head, revision); - } - } - return result; - } - - @Override - public boolean isMatch(@NonNull SCM scm) { - // TODO - return false; - } - - @Override - public Iterable getPullRequests(BitbucketSCMSource src) throws InterruptedException { - if (hookEvent == PULL_REQUEST_DECLINED || hookEvent == PULL_REQUEST_MERGED) { - return Collections.emptyList(); - } - return Collections.singleton(getPayload().getPullRequest()); - } + /* for test purpose */ + protected void notifyEvent(SCMHeadEvent event, int delaySeconds) { + SCMHeadEvent.fireLater(event, delaySeconds, TimeUnit.SECONDS); } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PushEvent.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PushEvent.java new file mode 100644 index 000000000..f06ba8779 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PushEvent.java @@ -0,0 +1,104 @@ +/* + * The MIT License + * + * Copyright (c) 2016, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.cloudbees.jenkins.plugins.bitbucket.hooks; + +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketTagSCMHead; +import com.cloudbees.jenkins.plugins.bitbucket.BranchSCMHead; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPushEvent; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPushEvent.Reference; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPushEvent.Target; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import jenkins.plugins.git.AbstractGitSCMSource; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMSource; + +final class PushEvent extends AbstractSCMHeadEvent { + + PushEvent(Type type, BitbucketPushEvent payload, String origin) { + super(type, payload, origin); + } + + @NonNull + @Override + public String getSourceName() { + return getRepository().getRepositoryName(); + } + + @NonNull + @Override + public Map heads(@NonNull SCMSource source) { + if (!(source instanceof BitbucketSCMSource)) { + return Collections.emptyMap(); + } + BitbucketSCMSource src = (BitbucketSCMSource) source; + if (!isServerURLMatch(src.getServerUrl())) { + return Collections.emptyMap(); + } + if (!src.getRepoOwner().equalsIgnoreCase(getPayload().getRepository().getOwnerName())) { + return Collections.emptyMap(); + } + if (!src.getRepository().equalsIgnoreCase(getPayload().getRepository().getRepositoryName())) { + return Collections.emptyMap(); + } + + Map result = new HashMap<>(); + for (BitbucketPushEvent.Change change: getPayload().getChanges()) { + if (change.isClosed()) { + result.put(new BranchSCMHead(change.getOld().getName()), null); + } else { + // created is true + Reference newChange = change.getNew(); + Target target = newChange.getTarget(); + + SCMHead head = null; + String eventType = newChange.getType(); + if ("tag".equals(eventType)) { + // for BB Cloud date is valued only in case of annotated tag + Date tagDate = newChange.getDate() != null ? newChange.getDate() : target.getDate(); + if (tagDate == null) { + // fall back to the jenkins time when the request is processed + tagDate = new Date(); + } + head = new BitbucketTagSCMHead(newChange.getName(), tagDate.getTime()); + } else { + head = new BranchSCMHead(newChange.getName()); + } + result.put(head, new AbstractGitSCMSource.SCMRevisionImpl(head, target.getHash())); + } + } + return result; + } + + @Override + protected BitbucketRepository getRepository() { + return getPayload().getRepository(); + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PushHookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PushHookProcessor.java index 8b12d127c..0328ffa8a 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PushHookProcessor.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PushHookProcessor.java @@ -23,40 +23,21 @@ */ package com.cloudbees.jenkins.plugins.bitbucket.hooks; -import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; -import com.cloudbees.jenkins.plugins.bitbucket.BitbucketTagSCMHead; -import com.cloudbees.jenkins.plugins.bitbucket.BranchSCMHead; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketHref; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPushEvent; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPushEvent.Reference; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPushEvent.Target; import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudWebhookPayload; -import com.cloudbees.jenkins.plugins.bitbucket.client.events.BitbucketCloudPushEvent; -import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerWebhookPayload; -import com.cloudbees.jenkins.plugins.bitbucket.server.events.BitbucketServerPushEvent; -import edu.umd.cs.findbugs.annotations.NonNull; -import hudson.scm.SCM; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import hudson.RestrictedSince; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; -import jenkins.plugins.git.AbstractGitSCMSource; import jenkins.scm.api.SCMEvent; -import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMHeadEvent; -import jenkins.scm.api.SCMNavigator; -import jenkins.scm.api.SCMRevision; -import jenkins.scm.api.SCMSource; -import org.apache.commons.lang.StringUtils; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +@Restricted(NoExternalUse.class) +@RestrictedSince("933.3.0") public class PushHookProcessor extends HookProcessor { private static final Logger LOGGER = Logger.getLogger(PushHookProcessor.class.getName()); @@ -66,6 +47,7 @@ public void process(HookEventType hookEvent, String payload, BitbucketType insta if (payload != null) { BitbucketPushEvent push; if (instanceType == BitbucketType.SERVER) { + // plugin webhook case push = BitbucketServerWebhookPayload.pushEventFromPayload(payload); } else { push = BitbucketCloudWebhookPayload.pushEventFromPayload(payload); @@ -88,123 +70,14 @@ public void process(HookEventType hookEvent, String payload, BitbucketType insta type = SCMEvent.Type.UPDATED; } } - SCMHeadEvent.fireLater(new SCMHeadEvent(type, push, origin) { - @Override - public boolean isMatch(@NonNull SCMNavigator navigator) { - if (!(navigator instanceof BitbucketSCMNavigator)) { - return false; - } - BitbucketSCMNavigator bbNav = (BitbucketSCMNavigator) navigator; - if (!isProjectKeyMatch(bbNav.getProjectKey())) { - return false; - } - if (!isServerUrlMatch(bbNav.getServerUrl())) { - return false; - } - return bbNav.getRepoOwner().equalsIgnoreCase(getPayload().getRepository().getOwnerName()); - } - - private boolean isProjectKeyMatch(String projectKey) { - if (StringUtils.isBlank(projectKey)) { - return true; - } - if (this.getPayload().getRepository().getProject() != null) { - return projectKey.equals(this.getPayload().getRepository().getProject().getKey()); - } - return true; - } - - private boolean isServerUrlMatch(String serverUrl) { - if (serverUrl == null || BitbucketCloudEndpoint.SERVER_URL.equals(serverUrl)) { - // this is a Bitbucket cloud navigator - if (getPayload() instanceof BitbucketServerPushEvent) { - return false; - } - } else { - // this is a Bitbucket server navigator - if (getPayload() instanceof BitbucketCloudPushEvent) { - return false; - } - Map> links = getPayload().getRepository().getLinks(); - if (links != null && links.containsKey("self")) { - boolean matches = false; - for (BitbucketHref link : links.get("self")) { - try { - URI navUri = new URI(serverUrl); - URI evtUri = new URI(link.getHref()); - if (navUri.getHost().equalsIgnoreCase(evtUri.getHost())) { - matches = true; - break; - } - } catch (URISyntaxException e) { - // ignore - } - } - return matches; - } - } - return true; - } - - @NonNull - @Override - public String getSourceName() { - return getPayload().getRepository().getRepositoryName(); - } - - @NonNull - @Override - public Map heads(@NonNull SCMSource source) { - if (!(source instanceof BitbucketSCMSource)) { - return Collections.emptyMap(); - } - BitbucketSCMSource src = (BitbucketSCMSource) source; - if (!isServerUrlMatch(src.getServerUrl())) { - return Collections.emptyMap(); - } - if (!src.getRepoOwner().equalsIgnoreCase(getPayload().getRepository().getOwnerName())) { - return Collections.emptyMap(); - } - if (!src.getRepository().equalsIgnoreCase(getPayload().getRepository().getRepositoryName())) { - return Collections.emptyMap(); - } - - Map result = new HashMap<>(); - for (BitbucketPushEvent.Change change: getPayload().getChanges()) { - if (change.isClosed()) { - result.put(new BranchSCMHead(change.getOld().getName()), null); - } else { - // created is true - Reference newChange = change.getNew(); - Target target = newChange.getTarget(); - - SCMHead head = null; - String eventType = newChange.getType(); - if ("tag".equals(eventType)) { - // for BB Cloud date is valued only in case of annotated tag - Date tagDate = newChange.getDate() != null ? newChange.getDate() : target.getDate(); - if (tagDate == null) { - // fall back to the jenkins time when the request is processed - tagDate = new Date(); - } - head = new BitbucketTagSCMHead(newChange.getName(), tagDate.getTime()); - } else { - head = new BranchSCMHead(newChange.getName()); - } - result.put(head, new AbstractGitSCMSource.SCMRevisionImpl(head, target.getHash())); - } - } - return result; - } - - @Override - public boolean isMatch(@NonNull SCM scm) { - // TODO - return false; - } - }, BitbucketSCMSource.getEventDelaySeconds(), TimeUnit.SECONDS); + SCMHeadEvent.fireLater(new PushEvent(type, push, origin), BitbucketSCMSource.getEventDelaySeconds(), TimeUnit.SECONDS); } } } } + + /* for test purpose */ + protected void notifyEvent(SCMHeadEvent event, int delaySeconds) { + SCMHeadEvent.fireLater(event, delaySeconds, TimeUnit.SECONDS); + } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/ServerHeadEvent.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/ServerHeadEvent.java new file mode 100644 index 000000000..c3535b906 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/ServerHeadEvent.java @@ -0,0 +1,120 @@ +/* + * The MIT License + * + * Copyright (c) 2016-2018, Yieldlab AG + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.cloudbees.jenkins.plugins.bitbucket.hooks; + +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSourceContext; +import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMHead; +import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMRevision; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; +import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerRepository; +import com.cloudbees.jenkins.plugins.bitbucket.server.events.NativeServerPullRequestEvent; +import com.google.common.base.Ascii; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import jenkins.plugins.git.AbstractGitSCMSource; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMHeadOrigin; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; + +final class ServerHeadEvent extends AbstractNativeServerSCMHeadEvent implements HasPullRequests { + ServerHeadEvent(String serverUrl, Type type, NativeServerPullRequestEvent payload, String origin) { + super(serverUrl, type, payload, origin); + } + + @Override + protected BitbucketServerRepository getRepository() { + return getPayload().getPullRequest().getDestination().getRepository(); + } + + @NonNull + @Override + protected Map heads(@NonNull BitbucketSCMSource source) { + if (!eventMatchesRepo(source)) { + return Collections.emptyMap(); + } + + final BitbucketSCMSourceContext ctx = contextOf(source); + if (!ctx.wantPRs()) { + return Collections.emptyMap(); // doesn't want PRs, nothing to do here + } + + final BitbucketPullRequest pullRequest = getPayload().getPullRequest(); + final BitbucketRepository sourceRepo = pullRequest.getSource().getRepository(); + final SCMHeadOrigin headOrigin = source.originOf(sourceRepo.getOwnerName(), sourceRepo.getRepositoryName()); + final Set strategies = headOrigin == SCMHeadOrigin.DEFAULT + ? ctx.originPRStrategies() + : ctx.forkPRStrategies(); + final Map result = new HashMap<>(strategies.size()); + for (final ChangeRequestCheckoutStrategy strategy : strategies) { + final String originalBranchName = pullRequest.getSource().getBranch().getName(); + final String branchName = String.format("PR-%s%s", pullRequest.getId(), + strategies.size() > 1 ? "-" + Ascii.toLowerCase(strategy.name()) : ""); + final PullRequestSCMHead head = new PullRequestSCMHead( + branchName, + sourceRepo.getOwnerName(), + sourceRepo.getRepositoryName(), + originalBranchName, + pullRequest, + headOrigin, + strategy + ); + + switch (getType()) { + case CREATED, + UPDATED: + final String targetHash = pullRequest.getDestination().getCommit().getHash(); + final String pullHash = pullRequest.getSource().getCommit().getHash(); + result.put(head, + new PullRequestSCMRevision(head, + new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), targetHash), + new AbstractGitSCMSource.SCMRevisionImpl(head, pullHash))); + break; + + case REMOVED: + // special case for repo being deleted + result.put(head, null); + break; + + default: + break; + } + } + + return result; + } + + @Override + public Iterable getPullRequests(BitbucketSCMSource src) throws InterruptedException { + if (Type.REMOVED.equals(getType())) { + return Collections.emptySet(); + } + return Collections.singleton(getPayload().getPullRequest()); + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/ServerPushEvent.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/ServerPushEvent.java new file mode 100644 index 000000000..39323d458 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/ServerPushEvent.java @@ -0,0 +1,285 @@ +/* + * The MIT License + * + * Copyright (c) 2016-2018, Yieldlab AG + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.cloudbees.jenkins.plugins.bitbucket.hooks; + +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSourceContext; +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketTagSCMHead; +import com.cloudbees.jenkins.plugins.bitbucket.BranchSCMHead; +import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMHead; +import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMRevision; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest; +import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerAPIClient; +import com.cloudbees.jenkins.plugins.bitbucket.server.client.pullrequest.BitbucketServerPullRequest; +import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerRepository; +import com.cloudbees.jenkins.plugins.bitbucket.server.events.NativeServerChange; +import com.google.common.base.Ascii; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.plugins.git.AbstractGitSCMSource; +import jenkins.scm.api.SCMEvent; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMHeadOrigin; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; + +import static java.util.Objects.requireNonNull; + +final class ServerPushEvent extends AbstractNativeServerSCMHeadEvent> implements HasPullRequests { + + private static final class CacheKey { + @NonNull + private final String refId; + @CheckForNull + private final String credentialsId; + + CacheKey(BitbucketSCMSource src, NativeServerChange change) { + this.refId = requireNonNull(change.getRefId()); + this.credentialsId = src.getCredentialsId(); + } + + @Override + public int hashCode() { + return Objects.hash(credentialsId, refId); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof CacheKey cacheKey) { + return Objects.equals(credentialsId, cacheKey.credentialsId) && refId.equals(cacheKey.refId); + } + + return false; + } + } + + // event logs with the name of the processor + private static final Logger LOGGER = Logger.getLogger(NativeServerPushHookProcessor.class.getName()); + + private final BitbucketServerRepository repository; + private final Map> cachedPullRequests = new HashMap<>(); + private final String mirrorId; + + ServerPushEvent(String serverUrl, + Type type, + Collection payload, + String origin, + BitbucketServerRepository repository, + String mirrorId) { + super(serverUrl, type, payload, origin); + this.repository = repository; + this.mirrorId = mirrorId; + } + + @Override + protected BitbucketServerRepository getRepository() { + return repository; + } + + @Override + protected Map heads(BitbucketSCMSource source) { + final Map result = new HashMap<>(); + if (!eventMatchesRepo(source)) { + return result; + } + + addBranchesAndTags(source, result); + try { + addPullRequests(source, result); + } catch (InterruptedException interrupted) { + LOGGER.log(Level.INFO, "Interrupted while fetching Pull Requests from Bitbucket, results may be incomplete."); + } + return result; + } + + private void addBranchesAndTags(BitbucketSCMSource src, Map result) { + for (final NativeServerChange change : getPayload()) { + String refType = change.getRef().getType(); + + if ("BRANCH".equals(refType)) { + final BranchSCMHead head = new BranchSCMHead(change.getRef().getDisplayId()); + final SCMRevision revision = getType() == SCMEvent.Type.REMOVED ? null + : new AbstractGitSCMSource.SCMRevisionImpl(head, change.getToHash()); + result.put(head, revision); + } else if ("TAG".equals(refType)) { + SCMHead head = new BitbucketTagSCMHead(change.getRef().getDisplayId(), 0); + final SCMRevision revision = getType() == SCMEvent.Type.REMOVED ? null + : new AbstractGitSCMSource.SCMRevisionImpl(head, change.getToHash()); + result.put(head, revision); + } else { + LOGGER.log(Level.INFO, "Received event for unknown ref type {0} of ref {1}", + new Object[] { change.getRef().getType(), change.getRef().getDisplayId() }); + } + } + } + + private void addPullRequests(BitbucketSCMSource src, Map result) throws InterruptedException { + if (getType() != SCMEvent.Type.UPDATED) { + return; // adds/deletes won't be handled here + } + + final BitbucketSCMSourceContext ctx = contextOf(src); + if (!ctx.wantPRs()) { + // doesn't want PRs, let the push event handle origin branches + return; + } + + final String sourceOwnerName = src.getRepoOwner(); + final String sourceRepoName = src.getRepository(); + final BitbucketServerRepository eventRepo = repository; + final SCMHeadOrigin headOrigin = src.originOf(eventRepo.getOwnerName(), eventRepo.getRepositoryName()); + final Set strategies = headOrigin == SCMHeadOrigin.DEFAULT + ? ctx.originPRStrategies() : ctx.forkPRStrategies(); + + for (final NativeServerChange change : getPayload()) { + if (!"BRANCH".equals(change.getRef().getType())) { + LOGGER.log(Level.INFO, "Received event for unknown ref type {0} of ref {1}", + new Object[] { change.getRef().getType(), change.getRef().getDisplayId() }); + continue; + } + + // iterate over all PRs in which this change is involved + for (final BitbucketServerPullRequest pullRequest : getPullRequests(src, change).values()) { + final BitbucketServerRepository targetRepo = pullRequest.getDestination().getRepository(); + // check if the target of the PR is actually this source + if (!sourceOwnerName.equalsIgnoreCase(targetRepo.getOwnerName()) + || !sourceRepoName.equalsIgnoreCase(targetRepo.getRepositoryName())) { + continue; + } + + for (final ChangeRequestCheckoutStrategy strategy : strategies) { + if (strategy != ChangeRequestCheckoutStrategy.MERGE && !change.getRefId().equals(pullRequest.getSource().getRefId())) { + continue; // Skip non-merge builds if the changed ref is not the source of the PR. + } + + final String originalBranchName = pullRequest.getSource().getBranch().getName(); + final String branchName = String.format("PR-%s%s", pullRequest.getId(), + strategies.size() > 1 ? "-" + Ascii.toLowerCase(strategy.name()) : ""); + + final BitbucketServerRepository pullRequestRepository = pullRequest.getSource().getRepository(); + final PullRequestSCMHead head = new PullRequestSCMHead( + branchName, + pullRequestRepository.getOwnerName(), + pullRequestRepository.getRepositoryName(), + originalBranchName, + pullRequest, + headOrigin, + strategy + ); + + final String targetHash = pullRequest.getDestination().getCommit().getHash(); + final String pullHash = pullRequest.getSource().getCommit().getHash(); + + result.put(head, + new PullRequestSCMRevision(head, + new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), targetHash), + new AbstractGitSCMSource.SCMRevisionImpl(head, pullHash))); + } + } + } + } + + private Map getPullRequests(BitbucketSCMSource src, NativeServerChange change) + throws InterruptedException { + + Map pullRequests; + final CacheKey cacheKey = new CacheKey(src, change); + synchronized (cachedPullRequests) { + pullRequests = cachedPullRequests.get(cacheKey); + if (pullRequests == null) { + cachedPullRequests.put(cacheKey, pullRequests = loadPullRequests(src, change)); + } + } + + return pullRequests; + } + + private Map loadPullRequests(BitbucketSCMSource src, + NativeServerChange change) throws InterruptedException { + + final BitbucketServerRepository eventRepo = repository; + final BitbucketServerAPIClient api = (BitbucketServerAPIClient) src + .buildBitbucketClient(eventRepo.getOwnerName(), eventRepo.getRepositoryName()); + + final Map pullRequests = new HashMap<>(); + + try { + try { + for (final BitbucketServerPullRequest pullRequest : api.getOutgoingOpenPullRequests(change.getRefId())) { + pullRequests.put(pullRequest.getId(), pullRequest); + } + } catch (final FileNotFoundException e) { + throw e; + } catch (IOException | RuntimeException e) { + LOGGER.log(Level.WARNING, "Failed to retrieve outgoing Pull Requests from Bitbucket", e); + } + + try { + for (final BitbucketServerPullRequest pullRequest : api.getIncomingOpenPullRequests(change.getRefId())) { + pullRequests.put(pullRequest.getId(), pullRequest); + } + } catch (final FileNotFoundException e) { + throw e; + } catch (IOException | RuntimeException e) { + LOGGER.log(Level.WARNING, "Failed to retrieve incoming Pull Requests from Bitbucket", e); + } + } catch (FileNotFoundException e) { + LOGGER.log(Level.INFO, "No such Repository on Bitbucket: {0}", e.getMessage()); + } + + return pullRequests; + } + + @Override + public Collection getPullRequests(BitbucketSCMSource src) throws InterruptedException { + List prs = new ArrayList<>(); + for (final NativeServerChange change : getPayload()) { + Map prsForChange = getPullRequests(src, change); + prs.addAll(prsForChange.values()); + } + + return prs; + } + + @Override + protected boolean eventMatchesRepo(BitbucketSCMSource source) { + return Objects.equals(source.getMirrorId(), this.mirrorId) && super.eventMatchesRepo(source); + } + +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfiguration.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfiguration.java index 43275f44a..fbd4deddf 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfiguration.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfiguration.java @@ -26,9 +26,9 @@ import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook; import com.cloudbees.jenkins.plugins.bitbucket.client.repository.BitbucketRepositoryHook; -import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketEndpointConfiguration; import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketServerEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils; import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerWebhook; import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.NativeBitbucketServerWebhook; import com.damnhandy.uri.template.UriTemplate; @@ -112,23 +112,20 @@ public String getCommittersToIgnore() { } boolean updateHook(BitbucketWebHook hook, BitbucketSCMSource owner) { - if (hook instanceof BitbucketRepositoryHook) { + if (hook instanceof BitbucketRepositoryHook cloudHook) { if (!hook.getEvents().containsAll(CLOUD_EVENTS)) { Set events = new TreeSet<>(hook.getEvents()); events.addAll(CLOUD_EVENTS); - BitbucketRepositoryHook repoHook = (BitbucketRepositoryHook) hook; - repoHook.setEvents(new ArrayList<>(events)); + cloudHook.setEvents(new ArrayList<>(events)); return true; } return false; } - if (hook instanceof BitbucketServerWebhook) { - BitbucketServerWebhook serverHook = (BitbucketServerWebhook) hook; - + if (hook instanceof BitbucketServerWebhook serverHook) { // Handle null case - String hookCommittersToIgnore = ((BitbucketServerWebhook) hook).getCommittersToIgnore(); + String hookCommittersToIgnore = serverHook.getCommittersToIgnore(); if (hookCommittersToIgnore == null) { hookCommittersToIgnore = ""; } @@ -147,10 +144,9 @@ boolean updateHook(BitbucketWebHook hook, BitbucketSCMSource owner) { return false; } - if (hook instanceof NativeBitbucketServerWebhook) { + if (hook instanceof NativeBitbucketServerWebhook serverHook) { boolean updated = false; - NativeBitbucketServerWebhook serverHook = (NativeBitbucketServerWebhook) hook; String serverUrl = owner.getServerUrl(); String url = getNativeServerWebhookUrl(serverUrl, owner.getEndpointJenkinsRootUrl()); @@ -180,7 +176,7 @@ public BitbucketWebHook getHook(BitbucketSCMSource owner) { final String serverUrl = owner.getServerUrl(); final String rootUrl = owner.getEndpointJenkinsRootUrl(); - if (BitbucketCloudEndpoint.SERVER_URL.equals(serverUrl)) { + if (BitbucketApiUtils.isCloud(serverUrl)) { BitbucketRepositoryHook hook = new BitbucketRepositoryHook(); hook.setEvents(CLOUD_EVENTS); hook.setActive(true); diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/util/URLUtils.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/util/URLUtils.java index ff2cbbbfb..643ff4dc3 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/util/URLUtils.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/util/URLUtils.java @@ -15,7 +15,7 @@ public static String removeAuthority(@CheckForNull String url) { if (url != null) { try { URL linkURL = new URL(url); - URL cleanURL = new URL(linkURL.getProtocol(), linkURL.getHost(), linkURL.getPort(), linkURL.getFile()); + URL cleanURL = new URL(linkURL.getProtocol(), linkURL.getHost(), linkURL.getPort(), /*linkURL.getPath()*/ linkURL.getFile()); return cleanURL.toExternalForm(); } catch (MalformedURLException e) { // do nothing, URL can not be parsed, leave as is diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/BitbucketSCMSourcePushHookReceiverTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/BitbucketSCMSourcePushHookReceiverTest.java new file mode 100644 index 000000000..6d57c8322 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/BitbucketSCMSourcePushHookReceiverTest.java @@ -0,0 +1,137 @@ +/* + * The MIT License + * + * Copyright (c) 2025, Nikolas Falco + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.cloudbees.jenkins.plugins.bitbucket.hooks; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import java.io.IOException; +import java.io.InputStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.kohsuke.stapler.StaplerRequest2; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class BitbucketSCMSourcePushHookReceiverTest { + + private BitbucketSCMSourcePushHookReceiver sut; + private StaplerRequest2 req; + private HookProcessor hookProcessor; + + @BeforeEach + void setup() { + req = mock(StaplerRequest2.class); + when(req.getRemoteHost()).thenReturn("https://bitbucket.org"); + when(req.getParameter("server_url")).thenReturn("https://bitbucket.org"); + when(req.getRemoteAddr()).thenReturn("[185.166.143.48"); + when(req.getScheme()).thenReturn("https"); + when(req.getServerName()).thenReturn("jenkins.example.com"); + when(req.getLocalPort()).thenReturn(80); + when(req.getRequestURI()).thenReturn("/bitbucket-scmsource-hook/notify"); + + sut = spy(new BitbucketSCMSourcePushHookReceiver()); + hookProcessor = mock(HookProcessor.class); + doReturn(hookProcessor).when(sut).getHookProcessor(any(HookEventType.class)); + } + + @Test + void test_pullrequest_created() throws Exception { + when(req.getHeader("X-Event-Key")).thenReturn("pullrequest:created"); + when(req.getHeader("X-Hook-UUID")).thenReturn("fc2f2c82-c7de-485b-917b-550c576751d3"); + when(req.getHeader("User-Agent")).thenReturn("Bitbucket-Webhooks/2.0"); + when(req.getHeader("X-Attempt-Number")).thenReturn("1"); + when(req.getHeader("X-Request-UUID")).thenReturn("5deabc5d-2369-4e11-a86a-396de804feca"); + when(req.getHeader("Content-Type")).thenReturn("application/json"); + when(req.getHeader("X-Bitbucket-Type")).thenReturn("cloud"); + when(req.getInputStream()).thenReturn(loadResource("pullrequest_created.json")); + + sut.doNotify(req); + + verify(sut).getHookProcessor(HookEventType.PULL_REQUEST_CREATED); + verify(hookProcessor).process( + eq(HookEventType.PULL_REQUEST_CREATED), + anyString(), + eq(BitbucketType.CLOUD), + eq("https://bitbucket.org/[185.166.143.48 ⇒ https://jenkins.example.com:80/bitbucket-scmsource-hook/notify"), + eq("https://bitbucket.org")); + } + + @Test + void test_pullrequest_declined() throws Exception { + when(req.getHeader("X-Event-Key")).thenReturn("pullrequest:rejected"); + when(req.getHeader("X-Hook-UUID")).thenReturn("fc2f2c82-c7de-485b-917b-450c576751d7"); + when(req.getHeader("User-Agent")).thenReturn("Bitbucket-Webhooks/2.0"); + when(req.getHeader("X-Attempt-Number")).thenReturn("1"); + when(req.getHeader("X-Request-UUID")).thenReturn("2b600570-77fa-4476-a091-a22b501a5542"); + when(req.getHeader("Content-Type")).thenReturn("application/json"); + when(req.getHeader("X-Bitbucket-Type")).thenReturn("cloud"); + when(req.getInputStream()).thenReturn(loadResource("pullrequest_rejected.json")); + + sut.doNotify(req); + + verify(sut).getHookProcessor(HookEventType.PULL_REQUEST_DECLINED); + verify(hookProcessor).process( + eq(HookEventType.PULL_REQUEST_DECLINED), + anyString(), + eq(BitbucketType.CLOUD), + eq("https://bitbucket.org/[185.166.143.48 ⇒ https://jenkins.example.com:80/bitbucket-scmsource-hook/notify"), + eq("https://bitbucket.org")); + } + + private ServletInputStream loadResource(String resource) { + final InputStream delegate = this.getClass().getResourceAsStream("cloud/" + resource); + return new ServletInputStream() { + + @Override + public int read() throws IOException { + return delegate.read(); + } + + @Override + public void setReadListener(ReadListener readListener) { + } + + @Override + public boolean isReady() { + return !isFinished(); + } + + @Override + public boolean isFinished() { + try { + return delegate.available() == 0; + } catch (IOException e) { + return false; + } + } + }; + } +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PullRequestHookProcessorTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PullRequestHookProcessorTest.java new file mode 100644 index 000000000..c9c6b85c3 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PullRequestHookProcessorTest.java @@ -0,0 +1,158 @@ +/* + * The MIT License + * + * Copyright (c) 2025, Nikolas Falco + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.cloudbees.jenkins.plugins.bitbucket.hooks; + +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator; +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.OriginPullRequestDiscoveryTrait; +import hudson.scm.SCM; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import jenkins.scm.api.SCMEvent.Type; +import jenkins.scm.api.SCMNavigator; +import jenkins.scm.api.SCMSource; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; +import org.mockito.ArgumentCaptor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +class PullRequestHookProcessorTest { + + private PullRequestHookProcessor sut; + + @BeforeEach + void setup() { + sut = spy(new PullRequestHookProcessor()); + } + + @Test + void test_pullrequest_created() throws Exception { + sut.process(HookEventType.PULL_REQUEST_CREATED, loadResource("pullrequest_created.json"), BitbucketType.CLOUD, "origin"); + + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(PREvent.class); + verify(sut).notifyEvent(eventCaptor.capture(), anyInt()); + PREvent event = eventCaptor.getValue(); + assertThat(event).isNotNull(); + assertThat(event.getSourceName()).isEqualTo("test-repos"); + assertThat(event.getType()).isEqualTo(Type.CREATED); + assertThat(event.isMatch(mock(SCM.class))).isFalse(); + } + + @Test + void test_pullrequest_rejected() throws Exception { + sut.process(HookEventType.PULL_REQUEST_DECLINED, loadResource("pullrequest_rejected.json"), BitbucketType.CLOUD, "origin"); + + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(PREvent.class); + verify(sut).notifyEvent(eventCaptor.capture(), anyInt()); + PREvent event = eventCaptor.getValue(); + assertThat(event).isNotNull(); + assertThat(event.getSourceName()).isEqualTo("test-repos"); + assertThat(event.getType()).isEqualTo(Type.REMOVED); + assertThat(event.isMatch(mock(SCM.class))).isFalse(); + } + + @Test + void test_pullrequest_created_when_event_match_SCMNavigator() throws Exception { + sut.process(HookEventType.PULL_REQUEST_CREATED, loadResource("pullrequest_created.json"), BitbucketType.CLOUD, "origin"); + + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(PREvent.class); + verify(sut).notifyEvent(eventCaptor.capture(), anyInt()); + PREvent event = eventCaptor.getValue(); + // discard any scm navigator than bitbucket + assertThat(event.isMatch(mock(SCMNavigator.class))).isFalse(); + + BitbucketSCMNavigator scmNavigator = new BitbucketSCMNavigator("amuniz"); + // cloud could not filter by ProjectKey + assertThat(event.isMatch(scmNavigator)).isTrue(); + // if set must match the project of repository from which the hook is generated + scmNavigator.setProjectKey("PRJKEY"); + assertThat(event.isMatch(scmNavigator)).isTrue(); + // project key is case sensitive + scmNavigator.setProjectKey("prjkey"); + assertThat(event.isMatch(scmNavigator)).isFalse(); + + // workspace/owner is case insensitive + scmNavigator = new BitbucketSCMNavigator("AMUNIZ"); + assertThat(event.isMatch(scmNavigator)).isTrue(); + } + + @WithJenkins + @Test + void test_pullrequest_created_when_event_match_SCMSource(JenkinsRule r) throws Exception { + sut.process(HookEventType.PULL_REQUEST_CREATED, loadResource("pullrequest_created.json"), BitbucketType.CLOUD, "origin"); + + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(PREvent.class); + verify(sut).notifyEvent(eventCaptor.capture(), anyInt()); + PREvent event = eventCaptor.getValue(); + // discard any scm navigator than bitbucket + assertThat(event.isMatch(mock(SCMSource.class))).isFalse(); + + BitbucketSCMSource scmSource = new BitbucketSCMSource("amuniz", "test-repos"); + // skip scm source that has not been configured to discover PRs + assertThat(event.isMatch(scmSource)).isFalse(); + + scmSource.setTraits(List.of(new OriginPullRequestDiscoveryTrait(2))); + assertThat(event.isMatch(scmSource)).isTrue(); + + // workspace/owner is case insensitive + scmSource = new BitbucketSCMSource("AMUNIZ", "TEST-REPOS"); + scmSource.setTraits(List.of(new OriginPullRequestDiscoveryTrait(1))); + assertThat(event.isMatch(scmSource)).isTrue(); + + assertThat(event.getPullRequests(scmSource)) + .isNotEmpty() + .hasSize(1); + } + + @WithJenkins + @Test + void test_pullrequest_rejected_returns_empty_pullrequests_when_event_match_SCMSource(JenkinsRule r) throws Exception { + sut.process(HookEventType.PULL_REQUEST_DECLINED, loadResource("pullrequest_rejected.json"), BitbucketType.CLOUD, "origin"); + + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(PREvent.class); + verify(sut).notifyEvent(eventCaptor.capture(), anyInt()); + PREvent event = eventCaptor.getValue(); + + BitbucketSCMSource scmSource = new BitbucketSCMSource("amuniz", "test-repos"); + scmSource.setTraits(List.of(new OriginPullRequestDiscoveryTrait(2))); + assertThat(event.isMatch(scmSource)).isTrue(); + assertThat(event.getPullRequests(scmSource)).isEmpty(); + } + + private String loadResource(String resource) throws IOException { + try (InputStream stream = this.getClass().getResourceAsStream("cloud/" + resource)) { + return IOUtils.toString(stream, StandardCharsets.UTF_8); + } + } +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfigurationTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfigurationTest.java index 0452dab85..92718b36b 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfigurationTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfigurationTest.java @@ -26,94 +26,94 @@ import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook; import io.jenkins.plugins.casc.ConfigurationAsCode; -import io.jenkins.plugins.casc.ConfiguratorException; -import org.junit.Before; -import org.junit.ClassRule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; -import org.mockito.Mockito; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class WebhookConfigurationTest { +@WithJenkins +class WebhookConfigurationTest { - @ClassRule - public static JenkinsRule j = new JenkinsRule(); + private static JenkinsRule r = new JenkinsRule(); - @Before - public void config() throws ConfiguratorException { - ConfigurationAsCode.get().configure( - getClass().getResource(getClass().getSimpleName() + "/configuration-as-code.yml").toString()); + @BeforeAll + static void init(JenkinsRule rule) { + r = rule; + String jcacURL = WebhookConfigurationTest.class.getResource( + WebhookConfigurationTest.class.getSimpleName() + "/configuration-as-code.yml").toString(); + ConfigurationAsCode.get().configure(jcacURL); } @Test - public void given_instanceWithServerVersion6_when_getHooks_SERVER_PR_RVWR_UPDATE_EVENT_exists() { + void given_instanceWithServerVersion6_when_getHooks_SERVER_PR_RVWR_UPDATE_EVENT_exists() { WebhookConfiguration whc = new WebhookConfiguration(); - BitbucketSCMSource owner = Mockito.mock(BitbucketSCMSource.class); + BitbucketSCMSource owner = mock(BitbucketSCMSource.class); final String server = "http://bitbucket.example.com:8088"; when(owner.getServerUrl()).thenReturn(server); when(owner.getEndpointJenkinsRootUrl()).thenReturn(server); BitbucketWebHook hook = whc.getHook(owner); - assertTrue(hook.getEvents().contains(HookEventType.SERVER_PULL_REQUEST_REVIEWER_UPDATED.getKey())); + assertThat(hook.getEvents()).contains(HookEventType.SERVER_PULL_REQUEST_REVIEWER_UPDATED.getKey()); } @Test - public void given_instanceWithServerVersion6_5_when_getHooks_SERVER_MIRROR_REPO_SYNC_EVENT_exists() { + void given_instanceWithServerVersion6_5_when_getHooks_SERVER_MIRROR_REPO_SYNC_EVENT_exists() { WebhookConfiguration whc = new WebhookConfiguration(); - BitbucketSCMSource owner = Mockito.mock(BitbucketSCMSource.class); + BitbucketSCMSource owner = mock(BitbucketSCMSource.class); final String server = "http://bitbucket.example.com:8091"; when(owner.getServerUrl()).thenReturn(server); when(owner.getEndpointJenkinsRootUrl()).thenReturn(server); when(owner.getEndpointJenkinsRootUrl()).thenReturn(server); when(owner.getMirrorId()).thenReturn("dummy-mirror-id"); BitbucketWebHook hook = whc.getHook(owner); - assertTrue(hook.getEvents().contains(HookEventType.SERVER_MIRROR_REPO_SYNCHRONIZED.getKey())); + assertThat(hook.getEvents()).contains(HookEventType.SERVER_MIRROR_REPO_SYNCHRONIZED.getKey()); } @Test - public void given_instanceWithServerVersion510_when_getHooks_SERVER_PR_RVWR_UPDATE_EVENT_exists() { + void given_instanceWithServerVersion510_when_getHooks_SERVER_PR_RVWR_UPDATE_EVENT_exists() { WebhookConfiguration whc = new WebhookConfiguration(); - BitbucketSCMSource owner = Mockito.mock(BitbucketSCMSource.class); + BitbucketSCMSource owner = mock(BitbucketSCMSource.class); final String server = "http://bitbucket.example.com:8089"; when(owner.getServerUrl()).thenReturn(server); when(owner.getEndpointJenkinsRootUrl()).thenReturn(server); BitbucketWebHook hook = whc.getHook(owner); - assertTrue(hook.getEvents().contains(HookEventType.SERVER_PULL_REQUEST_REVIEWER_UPDATED.getKey())); + assertThat(hook.getEvents()).contains(HookEventType.SERVER_PULL_REQUEST_REVIEWER_UPDATED.getKey()); } @Test - public void given_instanceWithServerVersion59_when_getHooks_SERVER_PR_RVWR_UPDATE_EVENT_not_exists() { + void given_instanceWithServerVersion59_when_getHooks_SERVER_PR_RVWR_UPDATE_EVENT_not_exists() { WebhookConfiguration whc = new WebhookConfiguration(); - BitbucketSCMSource owner = Mockito.mock(BitbucketSCMSource.class); + BitbucketSCMSource owner = mock(BitbucketSCMSource.class); final String server = "http://bitbucket.example.com:8090"; when(owner.getServerUrl()).thenReturn(server); when(owner.getEndpointJenkinsRootUrl()).thenReturn(server); BitbucketWebHook hook = whc.getHook(owner); - assertFalse(hook.getEvents().contains(HookEventType.SERVER_PULL_REQUEST_REVIEWER_UPDATED.getKey())); + assertThat(hook.getEvents()).doesNotContain(HookEventType.SERVER_PULL_REQUEST_REVIEWER_UPDATED.getKey()); } @Test - public void given_instanceWithServerVersion59_when_getHooks_SERVER_PR_MOD_EVENT_not_exists() { + void given_instanceWithServerVersion59_when_getHooks_SERVER_PR_MOD_EVENT_not_exists() { WebhookConfiguration whc = new WebhookConfiguration(); - BitbucketSCMSource owner = Mockito.mock(BitbucketSCMSource.class); + BitbucketSCMSource owner = mock(BitbucketSCMSource.class); final String server = "http://bitbucket.example.com:8090"; when(owner.getServerUrl()).thenReturn(server); when(owner.getEndpointJenkinsRootUrl()).thenReturn(server); BitbucketWebHook hook = whc.getHook(owner); - assertFalse(hook.getEvents().contains(HookEventType.SERVER_PULL_REQUEST_MODIFIED.getKey())); + assertThat(hook.getEvents()).doesNotContain(HookEventType.SERVER_PULL_REQUEST_MODIFIED.getKey()); } @Test - public void given_instanceWithServerVersion7_when_getHooks_SERVER_PR_FROM_REF_UPDATED_EVENT_exists() { + void given_instanceWithServerVersion7_when_getHooks_SERVER_PR_FROM_REF_UPDATED_EVENT_exists() { WebhookConfiguration whc = new WebhookConfiguration(); - BitbucketSCMSource owner = Mockito.mock(BitbucketSCMSource.class); + BitbucketSCMSource owner = mock(BitbucketSCMSource.class); final String server = "http://bitbucket.example.com:8087"; when(owner.getServerUrl()).thenReturn(server); when(owner.getEndpointJenkinsRootUrl()).thenReturn(server); BitbucketWebHook hook = whc.getHook(owner); - assertTrue(hook.getEvents().contains(HookEventType.SERVER_PULL_REQUEST_FROM_REF_UPDATED.getKey())); + assertThat(hook.getEvents()).contains(HookEventType.SERVER_PULL_REQUEST_FROM_REF_UPDATED.getKey()); } } diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/util/URLUtilsTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/util/URLUtilsTest.java new file mode 100644 index 000000000..ba7a2fd4b --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/impl/util/URLUtilsTest.java @@ -0,0 +1,22 @@ +package com.cloudbees.jenkins.plugins.bitbucket.impl.util; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class URLUtilsTest { + + @Test + void remove_authority_info() { + String url = URLUtils.removeAuthority("https://user:password@bitbucket.example.com/amuniz/test-repos?pagelen=100"); + assertThat(url).doesNotContain("username", "password"); + } + + @Disabled + @Test + void remove_jwt_query_param() { + String url = URLUtils.removeAuthority("https://bitbucket.example.com/rest/mirroring/latest/upstreamServers/1/repos/2?jwt=TOKEN"); + assertThat(url).doesNotContain("jwt", "TOKEN"); + } +} diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/cloud/pullrequest_created.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/cloud/pullrequest_created.json new file mode 100644 index 000000000..ad6a63b38 --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/cloud/pullrequest_created.json @@ -0,0 +1,258 @@ +{ + "actor": { + "account_id": "557058:ca1cd232-2017-4216-94be-99637899e18d", + "display_name": "Nikolas Falco", + "links": { + "avatar": { + "href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/NF-4.png" + }, + "html": { + "href": "https://bitbucket.org/%7B6b47f1dc-23a0-4b99-9964-d13e665f5d8e%7D/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B6b47f1dc-23a0-4b99-9964-d13e665f5d8e%7D" + } + }, + "nickname": "Nikolas Falco", + "type": "user", + "uuid": "{6b47f1dc-23a0-4b99-9964-d13e665f5d8e}" + }, + "pullrequest": { + "author": { + "account_id": "557058:ca1cd232-2017-4216-94be-99637899e18d", + "display_name": "Nikolas Falco", + "links": { + "avatar": { + "href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/NF-4.png" + }, + "html": { + "href": "https://bitbucket.org/%7B6b47f1dc-23a0-4b99-9964-d13e665f5d8e%7D/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B6b47f1dc-23a0-4b99-9964-d13e665f5d8e%7D" + } + }, + "nickname": "Nikolas Falco", + "type": "user", + "uuid": "{6b47f1dc-23a0-4b99-9964-d13e665f5d8e}" + }, + "close_source_branch": false, + "closed_by": null, + "comment_count": 0, + "created_on": "2024-11-09T13:02:56.285477+00:00", + "description": "", + "destination": { + "branch": { + "name": "master" + }, + "commit": { + "hash": "320cc39764dc", + "links": { + "html": { + "href": "https://bitbucket.org/amuniz/test-repos/commits/320cc39764dc" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/commit/320cc39764dc" + } + }, + "type": "commit" + }, + "repository": { + "full_name": "amuniz/test-repos", + "links": { + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bf991126e-6825-40ea-a1c2-e72c336ae41e%7D?ts=java" + }, + "html": { + "href": "https://bitbucket.org/amuniz/test-repos" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos" + } + }, + "name": "test-repos", + "type": "repository", + "uuid": "{f991126e-6825-40ea-a1c2-e72c336ae41e}" + } + }, + "id": 735, + "links": { + "activity": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/pullrequests/735/activity" + }, + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/pullrequests/735/approve" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/pullrequests/735/comments" + }, + "commits": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/pullrequests/735/commits" + }, + "decline": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/pullrequests/735/decline" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/diff/amuniz/test-repos:d31df3425c5b%0D320cc39764dc?from_pullrequest_id=735&topic=true" + }, + "diffstat": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/diffstat/amuniz/test-repos:d31df3425c5b%0D320cc39764dc?from_pullrequest_id=735&topic=true" + }, + "html": { + "href": "https://bitbucket.org/amuniz/test-repos/pull-requests/735" + }, + "merge": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/pullrequests/735/merge" + }, + "request-changes": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/pullrequests/735/request-changes" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/pullrequests/735" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/pullrequests/735/statuses" + } + }, + "merge_commit": null, + "participants": [], + "reason": "", + "rendered": { + "description": { + "html": "", + "markup": "markdown", + "raw": "", + "type": "rendered" + }, + "title": { + "html": "

BUILD test

", + "markup": "markdown", + "raw": "BUILD test", + "type": "rendered" + } + }, + "reviewers": [], + "source": { + "branch": { + "links": {}, + "name": "feature/test", + "sync_strategies": [ + "merge_commit", + "rebase" + ] + }, + "commit": { + "hash": "d31df3425c5b", + "links": { + "html": { + "href": "https://bitbucket.org/amuniz/test-repos/commits/d31df3425c5b" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/commit/d31df3425c5b" + } + }, + "type": "commit" + }, + "repository": { + "full_name": "amuniz/test-repos", + "links": { + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bf991126e-6825-40ea-a1c2-e72c336ae41e%7D?ts=java" + }, + "html": { + "href": "https://bitbucket.org/amuniz/test-repos" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos" + } + }, + "name": "test-repos", + "type": "repository", + "uuid": "{f991126e-6825-40ea-a1c2-e72c336ae41e}" + } + }, + "state": "OPEN", + "summary": { + "html": "", + "markup": "markdown", + "raw": "", + "type": "rendered" + }, + "task_count": 0, + "title": "BUILD test", + "type": "pullrequest", + "updated_on": "2024-11-09T13:02:57.098060+00:00" + }, + "repository": { + "full_name": "amuniz/test-repos", + "is_private": true, + "links": { + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bf991126e-6825-40ea-a1c2-e72c336ae41e%7D?ts=java" + }, + "html": { + "href": "https://bitbucket.org/amuniz/test-repos" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos" + } + }, + "name": "test-repos", + "owner": { + "display_name": "amuniz", + "links": { + "avatar": { + "href": "https://bitbucket.org/account/amuniz/avatar/" + }, + "html": { + "href": "https://bitbucket.org/%7B2f053ac1-4c69-4188-8014-43c976bf1c7c%7D/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/%7B2f053ac1-4c69-4188-8014-43c976bf1c7c%7D" + } + }, + "type": "team", + "username": "amuniz", + "uuid": "{2f053ac1-4c69-4188-8014-43c976bf1c7c}" + }, + "parent": null, + "project": { + "key": "PRJKEY", + "links": { + "avatar": { + "href": "https://bitbucket.org/account/user/amuniz/projects/PRJKEY/avatar/32?ts=1674583920" + }, + "html": { + "href": "https://bitbucket.org/amuniz/workspace/projects/PRJKEY" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/amuniz/projects/PRJKEY" + } + }, + "name": "My Project", + "type": "project", + "uuid": "{0a517fd3-65f0-473b-9bc1-bc1e8125519a}" + }, + "scm": "git", + "type": "repository", + "uuid": "{f991126e-6825-40ea-a1c2-e72c336ae41e}", + "website": null, + "workspace": { + "links": { + "avatar": { + "href": "https://bitbucket.org/workspaces/amuniz/avatar/?ts=1677575993" + }, + "html": { + "href": "https://bitbucket.org/amuniz/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/amuniz" + } + }, + "name": "amuniz", + "slug": "amuniz", + "type": "workspace", + "uuid": "{2f053ac1-4c69-4188-8014-43c976bf1c7c}" + } + } +} \ No newline at end of file diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/cloud/pullrequest_rejected.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/cloud/pullrequest_rejected.json new file mode 100644 index 000000000..c595c23ac --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/cloud/pullrequest_rejected.json @@ -0,0 +1,275 @@ +{ + "actor": { + "account_id": "557058:ca1cd232-2017-4216-94be-99637899e18d", + "display_name": "Nikolas Falco", + "links": { + "avatar": { + "href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/NF-4.png" + }, + "html": { + "href": "https://bitbucket.org/%7B6b47f1dc-23a0-4b99-9964-d13e665f5d8e%7D/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B6b47f1dc-23a0-4b99-9964-d13e665f5d8e%7D" + } + }, + "nickname": "Nikolas Falco", + "type": "user", + "uuid": "{6b47f1dc-23a0-4b99-9964-d13e665f5d8e}" + }, + "pullrequest": { + "author": { + "account_id": "557058:ca1cd232-2017-4216-94be-99637899e18d", + "display_name": "Nikolas Falco", + "links": { + "avatar": { + "href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/NF-4.png" + }, + "html": { + "href": "https://bitbucket.org/%7B6b47f1dc-23a0-4b99-9964-d13e665f5d8e%7D/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B6b47f1dc-23a0-4b99-9964-d13e665f5d8e%7D" + } + }, + "nickname": "Nikolas Falco", + "type": "user", + "uuid": "{6b47f1dc-23a0-4b99-9964-d13e665f5d8e}" + }, + "close_source_branch": false, + "closed_by": { + "account_id": "557058:ca1cd232-2017-4216-94be-99637899e18d", + "display_name": "Nikolas Falco", + "links": { + "avatar": { + "href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/NF-4.png" + }, + "html": { + "href": "https://bitbucket.org/%7B6b47f1dc-23a0-4b99-9964-d13e665f5d8e%7D/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B6b47f1dc-23a0-4b99-9964-d13e665f5d8e%7D" + } + }, + "nickname": "Nikolas Falco", + "type": "user", + "uuid": "{6b47f1dc-23a0-4b99-9964-d13e665f5d8e}" + }, + "comment_count": 0, + "created_on": "2024-11-09T13:02:56.285477+00:00", + "description": "", + "destination": { + "branch": { + "name": "master" + }, + "commit": { + "hash": "320cc39764dc", + "links": { + "html": { + "href": "https://bitbucket.org/amuniz/test-repos/commits/320cc39764dc" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/commit/320cc39764dc" + } + }, + "type": "commit" + }, + "repository": { + "full_name": "amuniz/test-repos", + "links": { + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bf991126e-6825-40ea-a1c2-e72c336ae41e%7D?ts=java" + }, + "html": { + "href": "https://bitbucket.org/amuniz/test-repos" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos" + } + }, + "name": "test-repos", + "type": "repository", + "uuid": "{f991126e-6825-40ea-a1c2-e72c336ae41e}" + } + }, + "id": 735, + "links": { + "activity": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/pullrequests/735/activity" + }, + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/pullrequests/735/approve" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/pullrequests/735/comments" + }, + "commits": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/pullrequests/735/commits" + }, + "decline": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/pullrequests/735/decline" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/diff/amuniz/test-repos:d31df3425c5b%0D320cc39764dc?from_pullrequest_id=735&topic=true" + }, + "diffstat": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/diffstat/amuniz/test-repos:d31df3425c5b%0D320cc39764dc?from_pullrequest_id=735&topic=true" + }, + "html": { + "href": "https://bitbucket.org/amuniz/test-repos/pull-requests/735" + }, + "merge": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/pullrequests/735/merge" + }, + "request-changes": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/pullrequests/735/request-changes" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/pullrequests/735" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/pullrequests/735/statuses" + } + }, + "merge_commit": null, + "participants": [], + "reason": "", + "rendered": { + "description": { + "html": "", + "markup": "markdown", + "raw": "", + "type": "rendered" + }, + "title": { + "html": "

BUILD test

", + "markup": "markdown", + "raw": "BUILD test", + "type": "rendered" + } + }, + "reviewers": [], + "source": { + "branch": { + "links": {}, + "name": "feature/test", + "sync_strategies": [ + "merge_commit", + "rebase" + ] + }, + "commit": { + "hash": "d31df3425c5b", + "links": { + "html": { + "href": "https://bitbucket.org/amuniz/test-repos/commits/d31df3425c5b" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos/commit/d31df3425c5b" + } + }, + "type": "commit" + }, + "repository": { + "full_name": "amuniz/test-repos", + "links": { + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bf991126e-6825-40ea-a1c2-e72c336ae41e%7D?ts=java" + }, + "html": { + "href": "https://bitbucket.org/amuniz/test-repos" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos" + } + }, + "name": "test-repos", + "type": "repository", + "uuid": "{f991126e-6825-40ea-a1c2-e72c336ae41e}" + } + }, + "state": "DECLINED", + "summary": { + "html": "", + "markup": "markdown", + "raw": "", + "type": "rendered" + }, + "task_count": 0, + "title": "BUILD test", + "type": "pullrequest", + "updated_on": "2024-11-09T13:14:33.659528+00:00" + }, + "repository": { + "full_name": "amuniz/test-repos", + "is_private": true, + "links": { + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bf991126e-6825-40ea-a1c2-e72c336ae41e%7D?ts=java" + }, + "html": { + "href": "https://bitbucket.org/amuniz/test-repos" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/amuniz/test-repos" + } + }, + "name": "test-repos", + "owner": { + "display_name": "amuniz", + "links": { + "avatar": { + "href": "https://bitbucket.org/account/amuniz/avatar/" + }, + "html": { + "href": "https://bitbucket.org/%7B2f053ac1-4c69-4188-8014-43c976bf1c7c%7D/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/%7B2f053ac1-4c69-4188-8014-43c976bf1c7c%7D" + } + }, + "type": "team", + "username": "amuniz", + "uuid": "{2f053ac1-4c69-4188-8014-43c976bf1c7c}" + }, + "parent": null, + "project": { + "key": "PRJKEY", + "links": { + "avatar": { + "href": "https://bitbucket.org/account/user/amuniz/projects/PRJKEY/avatar/32?ts=1674583920" + }, + "html": { + "href": "https://bitbucket.org/amuniz/workspace/projects/PRJKEY" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/amuniz/projects/PRJKEY" + } + }, + "name": "Polcity", + "type": "project", + "uuid": "{0a517fd3-65f0-473b-9bc1-bc1e8125519a}" + }, + "scm": "git", + "type": "repository", + "uuid": "{f991126e-6825-40ea-a1c2-e72c336ae41e}", + "website": null, + "workspace": { + "links": { + "avatar": { + "href": "https://bitbucket.org/workspaces/amuniz/avatar/?ts=1677575993" + }, + "html": { + "href": "https://bitbucket.org/amuniz/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/amuniz" + } + }, + "name": "amuniz", + "slug": "amuniz", + "type": "workspace", + "uuid": "{2f053ac1-4c69-4188-8014-43c976bf1c7c}" + } + } +} \ No newline at end of file