CVE-2024-28152
Description
In Jenkins Bitbucket Branch Source Plugin, the trust policy 'Forks in the same account' allows unauthorized Jenkinsfile changes from fork PRs on Bitbucket Server.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
In Jenkins Bitbucket Branch Source Plugin, the trust policy 'Forks in the same account' allows unauthorized Jenkinsfile changes from fork PRs on Bitbucket Server.
Vulnerability
Overview
The Jenkins Bitbucket Branch Source Plugin contains a flaw in its trust policy handling when discovering pull requests from forks. Specifically, the 'Forks in the same account' trust policy incorrectly permits users without write access to the project to modify Jenkinsfiles, but only when using Bitbucket Server [1][2]. This arises because the plugin does not properly verify write permissions for such users during the pull request discovery process.
Attack
Vector
To exploit this vulnerability, an attacker must have the ability to create a fork of a repository within the same Bitbucket Server account and submit a pull request containing a modified Jenkinsfile. The plugin's trust policy then accepts these changes without requiring the attacker to have write access to the target project [1][2]. No additional authentication bypass is needed beyond what is already granted to fork creators.
Impact
Successful exploitation allows an attacker to alter the Jenkinsfile used in builds triggered by the pull request. This could lead to arbitrary code execution on the Jenkins controller or agents, depending on the Jenkinsfile contents [1][2]. The impact is limited to pull requests from forks under the same account, but still represents a significant security risk.
Mitigation
The vulnerability is fixed in Bitbucket Branch Source Plugin version 871.v28d74e8b4226 and later [1][2]. Users are advised to update their plugin immediately. No workarounds are available for older versions [1].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.jenkins-ci.plugins:cloudbees-bitbucket-branch-sourceMaven | < 871.v28d74e8b_4226 | 871.v28d74e8b_4226 |
Affected products
2- ghsa-coordsRange: < 871.v28d74e8b_4226
- Range: 871.v28d74e8b_4226
Patches
128d74e8b4226Support cloning from Bitbucket mirror (#796)
48 files changed · +2120 −1137
docs/images/screenshot-13.png+0 −0 addeddocs/USER_GUIDE.adoc+19 −2 modified@@ -90,10 +90,10 @@ service to listen to these webhook requests and acts accordingly by triggering a triggering builds on matching branches or pull requests. Go to "Manage Jenkins" / "Configure System" and locate _Bitbucket Endpoints_. For every endpoint where you want webhooks registered automatically, -check "Manage hooks" and select a "Credential" with enough access to add webhooks to repositories. Since the Credential is used at the system level, +check "Manage hooks" and select a "Credential" with enough access to add webhooks to repositories. Since the Credential is used at the system level, it can be a System scoped credential, which will restrict its usage from Pipelines. -For both Bitbucket _Multibranch Pipelines_ and _Organization folders_ there is an "Override hook management" behavior +For both Bitbucket _Multibranch Pipelines_ and _Organization folders_ there is an "Override hook management" behavior to opt out or adjust system-wide settings. image::images/screenshot-4.png[scaledwidth=90%] @@ -152,6 +152,23 @@ image::images/screenshot-11.png[scaledwidth=90%] image::images/screenshot-12.png[scaledwidth=90%] +[id=bitbucket-mirror-support] +== Mirror support + +A mirrored Git repository can be configured for fetching references. + +The mirror is not used in the following cases: + +- If the source branch in a pull request resides in a different repository, the source branch is fetched from the primary repository while the target branch is fetched from the mirror. + +- During initial pull request scanning, the mirror isn't used because of the current design limitations. + +Cloning from the mirror can only be used with native web-hooks since plugin web-hooks don't provide a mirror identifier. + +For branches and tags, the mirror sync event is used. Thus, at cloning time, the mirror is already synchronized. However, in the case of a pull request event, there is no such guarantee. The plugin optimistically assumes that the mirror is synced and the required commit hashes exist in the mirrored repository at cloning time. If the plugin can't find the required hashes, it falls back to the primary repository. + +image::images/screenshot-13.png[scaledwidth=90%] + [id=bitbucket-misc-config] == Miscellaneous configuration
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java+0 −15 modified@@ -57,21 +57,6 @@ public interface BitbucketApi { @CheckForNull String getRepositoryName(); - /** - * Returns the URI of the repository. - * - * @param protocol the protocol to access the repository with. - * @param cloneLink the actual clone link for the repository as sent by the server, or {@code null} if unknown. - * @param owner the owner - * @param repository the repository. - * @return the repository URI. - */ - @NonNull - String getRepositoryUri(@NonNull BitbucketRepositoryProtocol protocol, - @CheckForNull String cloneLink, - @NonNull String owner, - @NonNull String repository); - /** * Returns the pull requests in the repository. *
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketMirroredRepositoryDescriptor.java+30 −0 added@@ -0,0 +1,30 @@ +package com.cloudbees.jenkins.plugins.bitbucket.api; + +import java.util.List; +import java.util.Map; + +/** + * Represents a Bitbucket mirror descriptor. + */ +public class BitbucketMirroredRepositoryDescriptor { + + private BitbucketMirrorServer mirrorServer; + + private Map<String, List<BitbucketHref>> links; + + public BitbucketMirrorServer getMirrorServer() { + return mirrorServer; + } + + public void setMirrorServer(BitbucketMirrorServer mirrorServer) { + this.mirrorServer = mirrorServer; + } + + public Map<String, List<BitbucketHref>> getLinks() { + return links; + } + + public void setLinks(Map<String, List<BitbucketHref>> links) { + this.links = links; + } +}
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketMirroredRepository.java+28 −0 added@@ -0,0 +1,28 @@ +package com.cloudbees.jenkins.plugins.bitbucket.api; + +import java.util.List; +import java.util.Map; + +public class BitbucketMirroredRepository { + + private boolean available; + + private Map<String, List<BitbucketHref>> links; + + public boolean isAvailable() { + return available; + } + + public void setAvailable(boolean available) { + this.available = available; + } + + public Map<String, List<BitbucketHref>> getLinks() { + return links; + } + + public void setLinks(Map<String, List<BitbucketHref>> links) { + this.links = links; + } + +}
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketMirrorServer.java+38 −0 added@@ -0,0 +1,38 @@ +package com.cloudbees.jenkins.plugins.bitbucket.api; + +/** + * Represents a Bitbucket mirror. + */ +public class BitbucketMirrorServer { + + private String id; + + private String name; + + private boolean enabled; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + +}
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketApiUtils.java+93 −0 added@@ -0,0 +1,93 @@ +package com.cloudbees.jenkins.plugins.bitbucket; + +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApiFactory; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRequestException; +import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketEndpointConfiguration; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import hudson.Util; +import hudson.model.Item; +import hudson.util.FormFillFailure; +import hudson.util.ListBoxModel; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.authentication.tokens.api.AuthenticationTokens; +import jenkins.model.Jenkins; +import jenkins.scm.api.SCMSourceOwner; +import org.apache.commons.lang.StringUtils; + +public class BitbucketApiUtils { + + private static final Logger LOGGER = Logger.getLogger(BitbucketApiUtils.class.getName()); + + public static ListBoxModel getFromBitbucket(SCMSourceOwner context, + String serverUrl, + String credentialsId, + String repoOwner, + String repository, + BitbucketSupplier<ListBoxModel> listBoxModelSupplier) + throws FormFillFailure { + repoOwner = Util.fixEmptyAndTrim(repoOwner); + if (repoOwner == null) { + return new ListBoxModel(); + } + if (context == null && !Jenkins.get().hasPermission(Jenkins.ADMINISTER) || + context != null && !context.hasPermission(Item.EXTENDED_READ)) { + return new ListBoxModel(); // not supposed to be seeing this form + } + if (context != null && !context.hasPermission(CredentialsProvider.USE_ITEM)) { + return new ListBoxModel(); // not permitted to try connecting with these credentials + } + + String serverUrlFallback = BitbucketCloudEndpoint.SERVER_URL; + // if at least one bitbucket server is configured use it instead of bitbucket cloud + if(BitbucketEndpointConfiguration.get().getEndpointItems().size() > 0){ + serverUrlFallback = BitbucketEndpointConfiguration.get().getEndpointItems().get(0).value; + } + + serverUrl = StringUtils.defaultIfBlank(serverUrl, serverUrlFallback); + StandardCredentials credentials = BitbucketCredentials.lookupCredentials( + serverUrl, + context, + credentialsId, + StandardCredentials.class + ); + + BitbucketAuthenticator authenticator = AuthenticationTokens.convert(BitbucketAuthenticator.authenticationContext(serverUrl), credentials); + + try { + BitbucketApi bitbucket = BitbucketApiFactory.newInstance(serverUrl, authenticator, repoOwner, null, repository); + return listBoxModelSupplier.get(bitbucket); + } catch (FormFillFailure | OutOfMemoryError e) { + throw e; + } catch (IOException e) { + if (e instanceof BitbucketRequestException) { + if (((BitbucketRequestException) e).getHttpCode() == 401) { + throw FormFillFailure.error(credentials == null + ? Messages.BitbucketSCMSource_UnauthorizedAnonymous(repoOwner) + : Messages.BitbucketSCMSource_UnauthorizedOwner(repoOwner)).withSelectionCleared(); + } + } else if (e.getCause() instanceof BitbucketRequestException) { + if (((BitbucketRequestException) e.getCause()).getHttpCode() == 401) { + throw FormFillFailure.error(credentials == null + ? Messages.BitbucketSCMSource_UnauthorizedAnonymous(repoOwner) + : Messages.BitbucketSCMSource_UnauthorizedOwner(repoOwner)).withSelectionCleared(); + } + } + LOGGER.log(Level.SEVERE, e.getMessage(), e); + throw FormFillFailure.error(e.getMessage()); + } catch (Throwable e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + throw FormFillFailure.error(e.getMessage()); + } + } + + public interface BitbucketSupplier<T> { + T get(BitbucketApi bitbucketApi) throws IOException, InterruptedException; + } + +}
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilder.java+134 −92 modified@@ -23,11 +23,9 @@ */ package com.cloudbees.jenkins.plugins.bitbucket; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketHref; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryProtocol; -import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudApiClient; import com.cloudbees.jenkins.plugins.bitbucket.endpoints.AbstractBitbucketEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketEndpointConfiguration; import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketServerEndpoint; @@ -40,12 +38,8 @@ import hudson.Util; import hudson.plugins.git.GitSCM; import hudson.plugins.git.browser.BitbucketWeb; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import jenkins.plugins.git.AbstractGitSCMSource; import jenkins.plugins.git.GitSCMBuilder; -import jenkins.plugins.git.MergeWithGitSCMExtension; import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMRevision; import jenkins.scm.api.SCMSource; @@ -68,11 +62,16 @@ public class BitbucketGitSCMBuilder extends GitSCMBuilder<BitbucketGitSCMBuilder private final BitbucketSCMSource scmSource; /** - * The clone links for cloning the source repository and origin pull requests (but links will need tweaks for - * fork pull requests) + * The clone links for primary repository */ @NonNull - private List<BitbucketHref> cloneLinks = Collections.emptyList(); + private List<BitbucketHref> primaryCloneLinks = List.of(); + + /** + * The clone links for mirror repository if it's configured + */ + @NonNull + private List<BitbucketHref> mirrorCloneLinks = List.of(); /** * The {@link BitbucketRepositoryProtocol} that should be used. @@ -96,21 +95,6 @@ public BitbucketGitSCMBuilder(@NonNull BitbucketSCMSource scmSource, @NonNull SC // we provide a dummy repository URL to the super constructor and then fix is afterwards once we have // the clone links super(head, revision, /*dummy value*/scmSource.getServerUrl(), credentialsId); - withoutRefSpecs(); - if (head instanceof PullRequestSCMHead) { - if (scmSource.buildBitbucketClient() instanceof BitbucketCloudApiClient) { - // TODO fix once Bitbucket Cloud has a fix for https://bitbucket.org/site/master/issues/5814 - String branchName = ((PullRequestSCMHead) head).getBranchName(); - withRefSpec("+refs/heads/" + branchName + ":refs/remotes/@{remote}/" + head.getName()); - } else { - String pullId = ((PullRequestSCMHead) head).getId(); - withRefSpec("+refs/pull-requests/" + pullId + "/from:refs/remotes/@{remote}/" + head.getName()); - } - } else if (head instanceof TagSCMHead ){ - withRefSpec("+refs/tags/" + head.getName() + ":refs/tags/" + head.getName()); - } else { - withRefSpec("+refs/heads/" + head.getName() + ":refs/remotes/@{remote}/" + head.getName()); - } this.scmSource = scmSource; AbstractBitbucketEndpoint endpoint = BitbucketEndpointConfiguration.get().findEndpoint(scmSource.getServerUrl()); @@ -130,11 +114,19 @@ public BitbucketGitSCMBuilder(@NonNull BitbucketSCMSource scmSource, @NonNull SC /** * Provides the clone links from the {@link BitbucketRepository} to allow inference of ports for different protocols. * - * @param cloneLinks the clone links. + * @param primaryCloneLinks the clone links for primary repository. + * @param mirrorCloneLinks the clone links for mirror repository if it's configured. * @return {@code this} for method chaining. */ - public BitbucketGitSCMBuilder withCloneLinks(List<BitbucketHref> cloneLinks) { - this.cloneLinks = new ArrayList<>(Util.fixNull(cloneLinks)); + public BitbucketGitSCMBuilder withCloneLinks( + @CheckForNull List<BitbucketHref> primaryCloneLinks, + @CheckForNull List<BitbucketHref> mirrorCloneLinks + ) { + if (primaryCloneLinks == null) { + throw new IllegalArgumentException("Primary clone links shouldn't be null"); + } + this.primaryCloneLinks = primaryCloneLinks; + this.mirrorCloneLinks = Util.fixNull(mirrorCloneLinks); return withBitbucketRemote(); } @@ -149,16 +141,6 @@ public BitbucketSCMSource scmSource() { return scmSource; } - /** - * Returns the clone links (possibly empty). - * - * @return the clone links (possibly empty). - */ - @NonNull - public List<BitbucketHref> cloneLinks() { - return Collections.unmodifiableList(cloneLinks); - } - /** * Configures the {@link IdCredentials#getId()} of the {@link Credentials} to use when connecting to the * {@link #remote()} @@ -206,70 +188,130 @@ public BitbucketGitSCMBuilder withCredentials(String credentialsId, BitbucketRep */ @NonNull public BitbucketGitSCMBuilder withBitbucketRemote() { - SCMHead h = head(); - String repoOwner; - String repository; - BitbucketApi bitbucket = scmSource().buildBitbucketClient(); - if (h instanceof PullRequestSCMHead && bitbucket instanceof BitbucketCloudApiClient) { - // TODO fix once Bitbucket Cloud has a fix for https://bitbucket.org/site/master/issues/5814 - repoOwner = ((PullRequestSCMHead) h).getRepoOwner(); - repository = ((PullRequestSCMHead) h).getRepository(); + SCMHead head = head(); + withoutRefSpecs(); + String headName = head.getName(); + if (head instanceof PullRequestSCMHead) { + withPullRequestRemote((PullRequestSCMHead) head, headName); + } else if (head instanceof TagSCMHead) { + withTagRemote(headName); } else { - // head instanceof BranchSCMHead - repoOwner = scmSource.getRepoOwner(); - repository = scmSource.getRepository(); + withBranchRemote(headName); } + return this; + } - String cloneLink = null; - for (BitbucketHref link : cloneLinks()) { - if (protocol.getType().equals(link.getName())) { - cloneLink = link.getHref(); - break; + private void withPullRequestRemote(PullRequestSCMHead head, String headName) { + String scmSourceRepoOwner = scmSource.getRepoOwner(); + String scmSourceRepository = scmSource.getRepository(); + String pullRequestRepoOwner = head.getRepoOwner(); + String pullRequestRepository = head.getRepository(); + boolean prFromTargetRepository = pullRequestRepoOwner.equals(scmSourceRepoOwner) + && pullRequestRepository.equals(scmSourceRepository); + SCMRevision revision = revision(); + ChangeRequestCheckoutStrategy checkoutStrategy = head.getCheckoutStrategy(); + // PullRequestSCMHead should be refactored to add references to target and source commit hashes. + // So revision should not be used here. There is a hack to use revision to get hashes. + boolean cloneFromMirror = prFromTargetRepository + && !mirrorCloneLinks.isEmpty() + && revision instanceof PullRequestSCMRevision; + String targetBranch = head.getTarget().getName(); + String branchName = head.getBranchName(); + if (prFromTargetRepository) { + withRefSpec("+refs/heads/" + branchName + ":refs/remotes/@{remote}/" + branchName); + if (cloneFromMirror) { + PullRequestSCMRevision pullRequestSCMRevision = (PullRequestSCMRevision) revision; + String primaryRemoteName = remoteName().equals("primary") ? "primary-primary" : "primary"; + String cloneLink = getCloneLink(primaryCloneLinks); + List<BranchWithHash> branchWithHashes; + if (checkoutStrategy == ChangeRequestCheckoutStrategy.MERGE) { + branchWithHashes = List.of( + new BranchWithHash(branchName, pullRequestSCMRevision.getPull().getHash()), + new BranchWithHash(targetBranch, pullRequestSCMRevision.getTargetImpl().getHash()) + ); + } else { + branchWithHashes = List.of( + new BranchWithHash(branchName, pullRequestSCMRevision.getPull().getHash()) + ); + } + withExtension(new FallbackToOtherRepositoryGitSCMExtension(cloneLink, primaryRemoteName, branchWithHashes)); + withMirrorRemote(); + } else { + withPrimaryRemote(); + } + } else { + if (scmSource.isCloud()) { + withRefSpec("+refs/heads/" + branchName + ":refs/remotes/@{remote}/" + headName); + String cloneLink = getCloudRepositoryUri(pullRequestRepoOwner, pullRequestRepository); + withRemote(cloneLink); + } else { + String pullId = head.getId(); + withRefSpec("+refs/pull-requests/" + pullId + "/from:refs/remotes/@{remote}/" + headName); + withPrimaryRemote(); } } - withRemote(bitbucket.getRepositoryUri( - protocol, - cloneLink, - repoOwner, - repository)); - AbstractBitbucketEndpoint endpoint = - BitbucketEndpointConfiguration.get().findEndpoint(scmSource.getServerUrl()); - if (endpoint == null) { - endpoint = new BitbucketServerEndpoint(null, scmSource.getServerUrl(), false, null); + if (head.getCheckoutStrategy() == ChangeRequestCheckoutStrategy.MERGE) { + String hash = revision instanceof PullRequestSCMRevision + ? ((PullRequestSCMRevision) revision).getTargetImpl().getHash() + : null; + String refSpec = "+refs/heads/" + targetBranch + ":refs/remotes/@{remote}/" + targetBranch; + if (!prFromTargetRepository && scmSource.isCloud()) { + String upstreamRemoteName = remoteName().equals("upstream") ? "upstream-upstream" : "upstream"; + withAdditionalRemote(upstreamRemoteName, getCloneLink(primaryCloneLinks), refSpec); + withExtension(new MergeWithGitSCMExtension("remotes/" + upstreamRemoteName + "/" + targetBranch, hash)); + } else { + withRefSpec(refSpec); + withExtension(new MergeWithGitSCMExtension("remotes/" + remoteName() + "/" + targetBranch, hash)); + } } - withBrowser(new BitbucketWeb( - endpoint.getRepositoryUrl( - repoOwner, - repository - ))); + } - // now, if we have to build a merge commit, let's ensure we build the merge commit! - SCMRevision r = revision(); - if (h instanceof PullRequestSCMHead) { - PullRequestSCMHead head = (PullRequestSCMHead) h; - if (head.getCheckoutStrategy() == ChangeRequestCheckoutStrategy.MERGE) { - String name = head.getTarget().getName(); - String localName = head.getBranchName().equals(name) ? "upstream-" + name : name; + @NonNull + public String getCloudRepositoryUri(@NonNull String owner, @NonNull String repository) { + switch (protocol) { + case HTTP: + return "https://bitbucket.org/" + owner + "/" + repository + ".git"; + case SSH: + return "ssh://git@bitbucket.org/" + owner + "/" + repository + ".git"; + default: + throw new IllegalArgumentException("Unsupported repository protocol: " + protocol); + } + } - String remoteName = remoteName().equals("upstream") ? "upstream-upstream" : "upstream"; - withAdditionalRemote(remoteName, - bitbucket.getRepositoryUri( - protocol, - cloneLink, - scmSource().getRepoOwner(), - scmSource().getRepository()), - "+refs/heads/" + name + ":refs/remotes/@{remote}/" + localName); - if ((r instanceof PullRequestSCMRevision) - && ((PullRequestSCMRevision) r).getTarget() instanceof AbstractGitSCMSource.SCMRevisionImpl) { - withExtension(new MergeWithGitSCMExtension("remotes/" + remoteName + "/" + localName, - ((AbstractGitSCMSource.SCMRevisionImpl) ((PullRequestSCMRevision) r).getTarget()) - .getHash())); - } else { - withExtension(new MergeWithGitSCMExtension("remotes/" + remoteName + "/" + localName, null)); - } - } + private void withTagRemote(String headName) { + withRefSpec("+refs/tags/" + headName + ":refs/tags/" + headName); + if (mirrorCloneLinks.isEmpty()) { + withPrimaryRemote(); + } else { + withMirrorRemote(); } - return this; + } + + private void withBranchRemote(String headName) { + withRefSpec("+refs/heads/" + headName + ":refs/remotes/@{remote}/" + headName); + if (mirrorCloneLinks.isEmpty()) { + withPrimaryRemote(); + } else { + withMirrorRemote(); + } + } + + private void withPrimaryRemote() { + String cloneLink = getCloneLink(primaryCloneLinks); + withRemote(cloneLink); + } + + private void withMirrorRemote() { + String cloneLink = getCloneLink(mirrorCloneLinks); + withRemote(cloneLink); + } + + private String getCloneLink(List<BitbucketHref> cloneLinks) { + return cloneLinks.stream() + .filter(link -> protocol.getType().equals(link.getName())) + .findAny() + .orElseThrow(() -> new IllegalStateException("Can't find clone link for protocol " + protocol)) + .getHref(); } /**
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java+40 −1 modified@@ -32,6 +32,8 @@ import com.cloudbees.jenkins.plugins.bitbucket.endpoints.AbstractBitbucketEndpoint; 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.server.BitbucketServerWebhookImplementation; import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerProject; import com.cloudbees.plugins.credentials.CredentialsNameProvider; import com.cloudbees.plugins.credentials.common.StandardCredentials; @@ -47,6 +49,7 @@ import hudson.model.TaskListener; import hudson.plugins.git.GitSCM; import hudson.security.AccessControlled; +import hudson.util.FormFillFailure; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import java.io.IOException; @@ -98,6 +101,8 @@ import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; +import static com.cloudbees.jenkins.plugins.bitbucket.BitbucketApiUtils.getFromBitbucket; + public class BitbucketSCMNavigator extends SCMNavigator { private static final Logger LOGGER = Logger.getLogger(BitbucketSCMSource.class.getName()); @@ -106,6 +111,8 @@ public class BitbucketSCMNavigator extends SCMNavigator { private String serverUrl; @CheckForNull private String credentialsId; + @CheckForNull + private String mirrorId; @NonNull private final String repoOwner; @CheckForNull @@ -209,6 +216,11 @@ public String getCredentialsId() { return credentialsId; } + @CheckForNull + public String getMirrorId() { + return mirrorId; + } + public String getRepoOwner() { return repoOwner; } @@ -233,6 +245,11 @@ public void setCredentialsId(@CheckForNull String credentialsId) { this.credentialsId = Util.fixEmpty(credentialsId); } + @DataBoundSetter + public void setMirrorId(String mirrorId) { + this.mirrorId = Util.fixEmpty(mirrorId); + } + /** * Sets the behavioural traits that are applied to this navigator and any {@link BitbucketSCMSource} instances it * discovers. The new traits will take effect on the next navigation through any of the @@ -633,12 +650,33 @@ public FormValidation doCheckCredentialsId(@AncestorInPath SCMSourceOwner contex return BitbucketCredentials.checkCredentialsId(context, value, serverUrl); } + @SuppressWarnings("unused") // used By stapler + public static FormValidation doCheckMirrorId(@QueryParameter String value, @QueryParameter String serverUrl) { + if (!value.isEmpty()) { + BitbucketServerWebhookImplementation webhookImplementation = + BitbucketServerEndpoint.findWebhookImplementation(serverUrl); + if (webhookImplementation == BitbucketServerWebhookImplementation.PLUGIN) { + return FormValidation.error("Mirror can only be used with native webhooks"); + } + } + return FormValidation.ok(); + } + @SuppressWarnings("unused") // used By stapler public ListBoxModel doFillCredentialsIdItems(@AncestorInPath SCMSourceOwner context, @QueryParameter String serverUrl) { return BitbucketCredentials.fillCredentialsIdItems(context, serverUrl); } + @SuppressWarnings("unused") // used By stapler + public ListBoxModel doFillMirrorIdItems(@AncestorInPath SCMSourceOwner context, + @QueryParameter String serverUrl, + @QueryParameter String credentialsId, + @QueryParameter String repoOwner) + throws FormFillFailure { + return getFromBitbucket(context, serverUrl, credentialsId, repoOwner, null, MirrorListSupplier.INSTANCE); + } + public List<NamedArrayList<? extends SCMTraitDescriptor<?>>> getTraitsDescriptorLists() { BitbucketSCMSource.DescriptorImpl sourceDescriptor = Jenkins.get().getDescriptorByType(BitbucketSCMSource.DescriptorImpl.class); @@ -821,7 +859,8 @@ public SCMSource create(@NonNull String projectName) throws IOException, Interru serverUrl, credentialsId, repoOwner, - projectName) + projectName, + mirrorId) .withRequest(request) .build(); }
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceBuilder.java+18 −1 modified@@ -49,6 +49,11 @@ public class BitbucketSCMSourceBuilder extends SCMSourceBuilder<BitbucketSCMSour */ @CheckForNull private final String credentialsId; + /** + * The mirror name or {@code null} to use. + */ + @CheckForNull + private final String mirrorId; /** * The repository owner. */ @@ -66,11 +71,12 @@ public class BitbucketSCMSourceBuilder extends SCMSourceBuilder<BitbucketSCMSour */ public BitbucketSCMSourceBuilder(@CheckForNull String id, @NonNull String serverUrl, @CheckForNull String credentialsId, @NonNull String repoOwner, - @NonNull String repoName) { + @NonNull String repoName, @CheckForNull String mirrorId) { super(BitbucketSCMSource.class, repoName); this.id = id; this.serverUrl = BitbucketEndpointConfiguration.normalizeServerUrl(serverUrl); this.credentialsId = credentialsId; + this.mirrorId = mirrorId; this.repoOwner = repoOwner; } @@ -94,6 +100,16 @@ public final String serverUrl() { return serverUrl; } + /** + * The mirror id that the {@link BitbucketSCMSource} will use. + * + * @return the mirror id that the {@link BitbucketSCMSource} will use. + */ + @CheckForNull + public final String mirrorId() { + return mirrorId; + } + /** * The credentials that the {@link BitbucketSCMSource} will use. * @@ -124,6 +140,7 @@ public BitbucketSCMSource build() { result.setId(id()); result.setServerUrl(serverUrl()); result.setCredentialsId(credentialsId()); + result.setMirrorId(mirrorId()); result.setTraits(traits()); return result; }
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java+169 −116 modified@@ -23,15 +23,17 @@ */ package com.cloudbees.jenkins.plugins.bitbucket; +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketApiUtils.BitbucketSupplier; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApiFactory; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBranch; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketCommit; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketHref; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketMirroredRepository; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketMirroredRepositoryDescriptor; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryProtocol; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryType; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRequestException; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketTeam; @@ -40,9 +42,12 @@ import com.cloudbees.jenkins.plugins.bitbucket.endpoints.AbstractBitbucketEndpoint; 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.hooks.HasPullRequests; +import com.cloudbees.jenkins.plugins.bitbucket.server.BitbucketServerWebhookImplementation; +import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerAPIClient; +import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerRepository; import com.cloudbees.plugins.credentials.CredentialsNameProvider; -import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.damnhandy.uri.template.UriTemplate; import com.fasterxml.jackson.databind.util.StdDateFormat; @@ -124,6 +129,8 @@ import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.interceptor.RequirePOST; +import static com.cloudbees.jenkins.plugins.bitbucket.BitbucketApiUtils.getFromBitbucket; + /** * SCM source implementation for Bitbucket. * @@ -155,6 +162,12 @@ public class BitbucketSCMSource extends SCMSource { @CheckForNull private String credentialsId; + /** + * Bitbucket mirror id + */ + @CheckForNull + private String mirrorId; + /** * Repository owner. * Used to build the repository URL. @@ -235,10 +248,15 @@ public class BitbucketSCMSource extends SCMSource { @CheckForNull private transient /*effectively final*/ Map<String, ContributorMetadataAction> pullRequestContributorCache; /** - * The cache of the clone links. + * The cache of the primary clone links. */ @CheckForNull - private transient List<BitbucketHref> cloneLinks = null; + private transient List<BitbucketHref> primaryCloneLinks = null; + /** + * The cache of the mirror clone links. + */ + @CheckForNull + private transient List<BitbucketHref> mirrorCloneLinks = null; /** * Constructor. @@ -321,6 +339,15 @@ public void setCredentialsId(@CheckForNull String credentialsId) { this.credentialsId = Util.fixEmpty(credentialsId); } + public String getMirrorId() { + return mirrorId; + } + + @DataBoundSetter + public void setMirrorId(String mirrorId) { + this.mirrorId = Util.fixEmpty(mirrorId); + } + @NonNull public String getRepoOwner() { return repoOwner; @@ -509,7 +536,7 @@ public BitbucketRepositoryType getRepositoryType() throws IOException, Interrupt repositoryType = BitbucketRepositoryType.fromString(r.getScm()); Map<String, List<BitbucketHref>> links = r.getLinks(); if (links != null && links.containsKey("clone")) { - cloneLinks = links.get("clone"); + primaryCloneLinks = links.get("clone"); } } return repositoryType; @@ -684,26 +711,15 @@ class Skip extends IOException { if (strategies.get(fork).size() > 1) { branchName = "PR-" + pull.getId() + "-" + strategy.name().toLowerCase(Locale.ENGLISH); } - PullRequestSCMHead head; - if (originBitbucket instanceof BitbucketCloudApiClient) { - head = new PullRequestSCMHead( // - branchName, // - pullRepoOwner, // - pullRepository, // - originalBranchName, // - pull, // - originOf(pullRepoOwner, pullRepository), // - strategy); - } else { - head = new PullRequestSCMHead( // - branchName, // - repoOwner, // - repository, // - originalBranchName, // - pull, // - originOf(pullRepoOwner, pullRepository), // - strategy); - } + PullRequestSCMHead head = new PullRequestSCMHead( // + branchName, // + pullRepoOwner, // + pullRepository, // + originalBranchName, // + pull, // + originOf(pullRepoOwner, pullRepository), // + strategy + ); if (request.process(head, // () -> { // use branch instead of commit to postpone closure initialisation @@ -769,10 +785,6 @@ private void retrieveBranches(final BitbucketSCMSourceRequest request) request.listener().getLogger().println("Looking up " + fullName + " for branches"); final BitbucketApi bitbucket = buildBitbucketClient(); - Map<String, List<BitbucketHref>> links = bitbucket.getRepository().getLinks(); - if (links != null && links.containsKey("clone")) { - cloneLinks = links.get("clone"); - } int count = 0; for (final BitbucketBranch branch : request.getBranches()) { request.listener().getLogger().println("Checking branch " + branch.getName() + " from " + fullName); @@ -795,10 +807,6 @@ private void retrieveTags(final BitbucketSCMSourceRequest request) throws IOExce request.listener().getLogger().println("Looking up " + fullName + " for tags"); final BitbucketApi bitbucket = buildBitbucketClient(); - Map<String, List<BitbucketHref>> links = bitbucket.getRepository().getLinks(); - if (links != null && links.containsKey("clone")) { - cloneLinks = links.get("clone"); - } int count = 0; for (final BitbucketBranch tag : request.getTags()) { request.listener().getLogger().println("Checking tag " + tag.getName() + " from " + fullName); @@ -888,7 +896,7 @@ protected SCMRevision retrieve(SCMHead head, TaskListener listener) throws IOExc return null; } - return new PullRequestSCMRevision<>( + return new PullRequestSCMRevision( h, new BitbucketGitSCMRevision(h.getTarget(), targetRevision), new BitbucketGitSCMRevision(h, sourceRevision) @@ -1016,36 +1024,13 @@ public SCM build(SCMHead head, SCMRevision revision) { } } assert type != null; + initCloneLinks(); - if (cloneLinks == null) { - BitbucketApi bitbucket = buildBitbucketClient(); - try { - BitbucketRepository r = bitbucket.getRepository(); - Map<String, List<BitbucketHref>> links = r.getLinks(); - if (links != null && links.containsKey("clone")) { - cloneLinks = links.get("clone"); - } - } catch (IOException | InterruptedException e) { - LOGGER.log(Level.SEVERE, - "Could not determine clone links of " + getRepoOwner() + "/" + getRepository() - + " on " + getServerUrl() + " for " + getOwner() + " falling back to generated links", - e); - cloneLinks = new ArrayList<>(); - cloneLinks.add(new BitbucketHref("ssh", - bitbucket.getRepositoryUri(BitbucketRepositoryProtocol.SSH, null, - getRepoOwner(), getRepository()) - )); - cloneLinks.add(new BitbucketHref("https", - bitbucket.getRepositoryUri(BitbucketRepositoryProtocol.HTTP, null, - getRepoOwner(), getRepository()) - )); - } - } switch (type) { case GIT: default: return new BitbucketGitSCMBuilder(this, head, revision, getCredentialsId()) - .withCloneLinks(cloneLinks) + .withCloneLinks(primaryCloneLinks, mirrorCloneLinks) .withTraits(traits) .build(); @@ -1068,7 +1053,7 @@ public SCMRevision getTrustedRevision(@NonNull SCMRevision revision, @NonNull Ta } catch (WrappedException wrapped) { wrapped.unwrap(); } - PullRequestSCMRevision<?> rev = (PullRequestSCMRevision) revision; + PullRequestSCMRevision rev = (PullRequestSCMRevision) revision; listener.getLogger().format("Loading trusted files from base branch %s at %s rather than %s%n", head.getTarget().getName(), rev.getTarget(), rev.getPull()); return rev.getTarget(); @@ -1107,7 +1092,7 @@ protected List<Action> retrieveActions(@CheckForNull SCMSourceEvent event, BitbucketRepository r = bitbucket.getRepository(); Map<String, List<BitbucketHref>> links = r.getLinks(); if (links != null && links.containsKey("clone")) { - cloneLinks = links.get("clone"); + primaryCloneLinks = links.get("clone"); } result.add(new BitbucketRepoMetadataAction(r)); String defaultBranch = bitbucket.getDefaultBranch(); @@ -1236,6 +1221,99 @@ public static void setEventDelaySeconds(int eventDelaySeconds) { BitbucketSCMSource.eventDelaySeconds = Math.min(300, Math.max(0, eventDelaySeconds)); } + private void initCloneLinks() { + if (primaryCloneLinks == null) { + BitbucketApi bitbucket = buildBitbucketClient(); + initPrimaryCloneLinks(bitbucket); + if (mirrorId != null && mirrorCloneLinks == null) { + initMirrorCloneLinks((BitbucketServerAPIClient) bitbucket, mirrorId); + } + } + if (mirrorId != null && mirrorCloneLinks == null) { + BitbucketApi bitbucket = buildBitbucketClient(); + initMirrorCloneLinks((BitbucketServerAPIClient) bitbucket, mirrorId); + } + } + + private void initMirrorCloneLinks(BitbucketServerAPIClient bitbucket, String mirrorIdLocal) { + try { + mirrorCloneLinks = getCloneLinksFromMirror(bitbucket, mirrorIdLocal); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, + "Could not determine mirror clone links of " + getRepoOwner() + "/" + getRepository() + + " on " + getServerUrl() + " for " + getOwner() + " falling back to primary server", + e); + } + } + + private List<BitbucketHref> getCloneLinksFromMirror( + BitbucketServerAPIClient bitbucket, + String mirrorIdLocal + ) throws IOException, InterruptedException { + // Mirrors are supported only by Bitbucket Server + BitbucketServerRepository r = (BitbucketServerRepository) bitbucket.getRepository(); + List<BitbucketMirroredRepositoryDescriptor> mirrors = bitbucket.getMirrors(r.getId()); + BitbucketMirroredRepositoryDescriptor mirroredRepositoryDescriptor = mirrors.stream() + .filter(it -> mirrorIdLocal.equals(it.getMirrorServer().getId())) + .findFirst() + .orElseThrow(() -> + new IllegalStateException("Could not find mirror descriptor for mirror id " + mirrorIdLocal) + ); + if (!mirroredRepositoryDescriptor.getMirrorServer().isEnabled()) { + throw new IllegalStateException("Mirror is disabled for mirror id " + mirrorIdLocal); + } + Map<String, List<BitbucketHref>> mirrorDescriptorLinks = mirroredRepositoryDescriptor.getLinks(); + if (mirrorDescriptorLinks == null) { + throw new IllegalStateException("There is no repository descriptor links for mirror id " + mirrorIdLocal); + } + List<BitbucketHref> self = mirrorDescriptorLinks.get("self"); + if (self == null || self.isEmpty()) { + throw new IllegalStateException("There is no self-link for mirror id " + mirrorIdLocal); + } + String selfLink = self.get(0).getHref(); + BitbucketMirroredRepository mirroredRepository = bitbucket.getMirroredRepository(selfLink); + if (!mirroredRepository.isAvailable()) { + throw new IllegalStateException("Mirrored repository is not available for mirror id " + mirrorIdLocal); + } + Map<String, List<BitbucketHref>> mirroredRepositoryLinks = mirroredRepository.getLinks(); + if (mirroredRepositoryLinks == null) { + throw new IllegalStateException("There is no mirrored repository links for mirror id " + mirrorIdLocal); + } + List<BitbucketHref> mirroredRepositoryCloneLinks = mirroredRepositoryLinks.get("clone"); + if (mirroredRepositoryCloneLinks == null) { + throw new IllegalStateException("There is no mirrored repository clone links for mirror id " + mirrorIdLocal); + } + return mirroredRepositoryCloneLinks; + } + + private void initPrimaryCloneLinks(BitbucketApi bitbucket) { + try { + primaryCloneLinks = getCloneLinksFromPrimary(bitbucket); + } catch (Exception e) { + throw new IllegalStateException( + "Could not determine clone links of " + getRepoOwner() + "/" + getRepository() + + " on " + getServerUrl() + " for " + getOwner() + " falling back to generated links", + e); + } + } + + private List<BitbucketHref> getCloneLinksFromPrimary(BitbucketApi bitbucket) throws IOException, InterruptedException { + BitbucketRepository r = bitbucket.getRepository(); + Map<String, List<BitbucketHref>> links = r.getLinks(); + if (links == null) { + throw new IllegalStateException("There is no links"); + } + List<BitbucketHref> cloneLinksLocal = links.get("clone"); + if (cloneLinksLocal == null) { + throw new IllegalStateException("There is no clone links"); + } + return cloneLinksLocal; + } + + public boolean isCloud() { + return BitbucketCloudEndpoint.SERVER_URL.equals(serverUrl); + } + @Symbol("bitbucket") @Extension public static class DescriptorImpl extends SCMSourceDescriptor { @@ -1265,6 +1343,18 @@ public static FormValidation doCheckServerUrl(@AncestorInPath SCMSourceOwner con return FormValidation.ok(); } + @SuppressWarnings("unused") // used By stapler + public static FormValidation doCheckMirrorId(@QueryParameter String value, @QueryParameter String serverUrl) { + if (!value.isEmpty()) { + BitbucketServerWebhookImplementation webhookImplementation = + BitbucketServerEndpoint.findWebhookImplementation(serverUrl); + if (webhookImplementation == BitbucketServerWebhookImplementation.PLUGIN) { + return FormValidation.error("Mirror can only be used with native webhooks"); + } + } + return FormValidation.ok(); + } + @SuppressWarnings("unused") // used By stapler public boolean isServerUrlSelectable() { return BitbucketEndpointConfiguration.get().isEndpointSelectable(); @@ -1291,70 +1381,33 @@ public ListBoxModel doFillRepositoryItems(@AncestorInPath SCMSourceOwner context @QueryParameter String credentialsId, @QueryParameter String repoOwner) throws IOException, InterruptedException { - repoOwner = Util.fixEmptyAndTrim(repoOwner); - if (repoOwner == null) { - return new ListBoxModel(); - } - if (context == null && !Jenkins.get().hasPermission(Jenkins.ADMINISTER) || - context != null && !context.hasPermission(Item.EXTENDED_READ)) { - return new ListBoxModel(); // not supposed to be seeing this form - } - if (context != null && !context.hasPermission(CredentialsProvider.USE_ITEM)) { - return new ListBoxModel(); // not permitted to try connecting with these credentials - } - - String serverUrlFallback = BitbucketCloudEndpoint.SERVER_URL; - // if at least one bitbucket server is configured use it instead of bitbucket cloud - if(BitbucketEndpointConfiguration.get().getEndpointItems().size() > 0){ - serverUrlFallback = BitbucketEndpointConfiguration.get().getEndpointItems().get(0).value; - } - - serverUrl = StringUtils.defaultIfBlank(serverUrl, serverUrlFallback); - ListBoxModel result = new ListBoxModel(); - StandardCredentials credentials = BitbucketCredentials.lookupCredentials( - serverUrl, - context, - credentialsId, - StandardCredentials.class - ); - - BitbucketAuthenticator authenticator = AuthenticationTokens.convert(BitbucketAuthenticator.authenticationContext(serverUrl), credentials); - - try { - BitbucketApi bitbucket = BitbucketApiFactory.newInstance(serverUrl, authenticator, repoOwner, null, null); + BitbucketSupplier<ListBoxModel> listBoxModelSupplier = bitbucket -> { + ListBoxModel result = new ListBoxModel(); BitbucketTeam team = bitbucket.getTeam(); List<? extends BitbucketRepository> repositories = - bitbucket.getRepositories(team != null ? null : UserRoleInRepository.CONTRIBUTOR); + bitbucket.getRepositories(team != null ? null : UserRoleInRepository.CONTRIBUTOR); if (repositories.isEmpty()) { throw FormFillFailure.error(Messages.BitbucketSCMSource_NoMatchingOwner(repoOwner)).withSelectionCleared(); } for (BitbucketRepository repo : repositories) { result.add(repo.getRepositoryName()); } return result; - } catch (FormFillFailure | OutOfMemoryError e) { - throw e; - } catch (IOException e) { - if (e instanceof BitbucketRequestException) { - if (((BitbucketRequestException) e).getHttpCode() == 401) { - throw FormFillFailure.error(credentials == null - ? Messages.BitbucketSCMSource_UnauthorizedAnonymous(repoOwner) - : Messages.BitbucketSCMSource_UnauthorizedOwner(repoOwner)).withSelectionCleared(); - } - } else if (e.getCause() instanceof BitbucketRequestException) { - if (((BitbucketRequestException) e.getCause()).getHttpCode() == 401) { - throw FormFillFailure.error(credentials == null - ? Messages.BitbucketSCMSource_UnauthorizedAnonymous(repoOwner) - : Messages.BitbucketSCMSource_UnauthorizedOwner(repoOwner)).withSelectionCleared(); - } - } - LOGGER.log(Level.SEVERE, e.getMessage(), e); - throw FormFillFailure.error(e.getMessage()); - } catch (Throwable e) { - LOGGER.log(Level.SEVERE, e.getMessage(), e); - throw FormFillFailure.error(e.getMessage()); - } + }; + return getFromBitbucket(context, serverUrl, credentialsId, repoOwner, null, listBoxModelSupplier); } + + @SuppressWarnings("unused") // used By stapler + public ListBoxModel doFillMirrorIdItems(@AncestorInPath SCMSourceOwner context, + @QueryParameter String serverUrl, + @QueryParameter String credentialsId, + @QueryParameter String repoOwner, + @QueryParameter String repository) + throws FormFillFailure { + + return getFromBitbucket(context, serverUrl, credentialsId, repoOwner, repository, MirrorListSupplier.INSTANCE); + } + @NonNull @Override protected SCMHeadCategory[] createCategories() { @@ -1518,7 +1571,7 @@ public SCMRevision create(@NonNull SCMHead head, PullRequestSCMHead prHead = (PullRequestSCMHead) head; SCMHead targetHead = prHead.getTarget(); - return new PullRequestSCMRevision<>( // + return new PullRequestSCMRevision( // prHead, // new BitbucketGitSCMRevision(targetHead, targetCommit), // new BitbucketGitSCMRevision(prHead, sourceCommit));
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BranchWithHash.java+19 −0 added@@ -0,0 +1,19 @@ +package com.cloudbees.jenkins.plugins.bitbucket; + +public class BranchWithHash { + private final String branch; + private final String hash; + + public BranchWithHash(String branch, String hash) { + this.branch = branch; + this.hash = hash; + } + + public String getBranch() { + return branch; + } + + public String getHash() { + return hash; + } +}
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java+0 −27 modified@@ -33,7 +33,6 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketException; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryProtocol; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRequestException; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketTeam; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook; @@ -231,32 +230,6 @@ public String getRepositoryName() { return repositoryName; } - /** - * {@inheritDoc} - */ - @NonNull - @Override - public String getRepositoryUri(@NonNull BitbucketRepositoryProtocol protocol, - @CheckForNull String cloneLink, - @NonNull String owner, - @NonNull String repository) { - // ignore port override on Cloud - switch (protocol) { - case HTTP: - if (authenticator != null) { - String username = authenticator.getUserUri(); - if (!username.isEmpty()) { - return "https://" + username + "@bitbucket.org/" + owner + "/" + repository + ".git"; - } - } - return "https://bitbucket.org/" + owner + "/" + repository + ".git"; - case SSH: - return "git@bitbucket.org:" + owner + "/" + repository + ".git"; - default: - throw new IllegalArgumentException("Unsupported repository protocol: " + protocol); - } - } - /** * {@inheritDoc} */
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/FallbackToOtherRepositoryGitSCMExtension.java+77 −0 added@@ -0,0 +1,77 @@ +package com.cloudbees.jenkins.plugins.bitbucket; + +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.git.GitException; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.Revision; +import hudson.plugins.git.extensions.GitSCMExtension; +import java.net.URISyntaxException; +import java.util.List; +import java.util.stream.Collectors; +import org.eclipse.jgit.transport.RefSpec; +import org.eclipse.jgit.transport.URIish; +import org.jenkinsci.plugins.gitclient.FetchCommand; +import org.jenkinsci.plugins.gitclient.GitClient; + +/** + * If specified commit hashes are not found in repository then fetch + * specified branches from remote. + */ +public class FallbackToOtherRepositoryGitSCMExtension extends GitSCMExtension { + + private final String cloneLink; + private final String remoteName; + private final List<BranchWithHash> branchWithHashes; + + public FallbackToOtherRepositoryGitSCMExtension( + String cloneLink, + String remoteName, + List<BranchWithHash> branchWithHashes + ) { + this.cloneLink = cloneLink; + this.remoteName = remoteName; + this.branchWithHashes = branchWithHashes; + } + + @Override + public Revision decorateRevisionToBuild( + GitSCM scm, + Run<?, ?> build, + GitClient git, + TaskListener listener, + Revision marked, + Revision rev + ) throws InterruptedException { + List<RefSpec> refSpecs = branchWithHashes.stream() + .filter(branchWithHash -> !commitExists(git, branchWithHash.getHash())) + .map(branchWithHash -> { + String branch = branchWithHash.getBranch(); + return new RefSpec("+refs/heads/" + branch + ":refs/remotes/" + remoteName + "/" + branch); + }) + .collect(Collectors.toList()); + + if (!refSpecs.isEmpty()) { + FetchCommand fetchCommand = git.fetch_(); + URIish remote; + try { + remote = new URIish(cloneLink); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + fetchCommand.from(remote, refSpecs).execute(); + } + return rev; + } + + private static boolean commitExists(GitClient git, String sha1) { + try { + git.revParse(sha1); + return true; + } catch (GitException ignored) { + return false; + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +}
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/HookEventType.java+6 −0 modified@@ -67,6 +67,12 @@ public enum HookEventType { */ SERVER_REFS_CHANGED("repo:refs_changed", NativeServerPushHookProcessor.class), + /** + * @see <a href="https://confluence.atlassian.com/bitbucketserver/event-payload-938025882.html#Eventpayload-repo-mirr-syn">Eventpayload-repo-mirr-syn</a> + * @since Bitbucket Server 6.5 + */ + SERVER_MIRROR_REPO_SYNCHRONIZED("mirror:repo_synchronized", NativeServerPushHookProcessor.class), + /** * @see <a href="https://confluence.atlassian.com/bitbucketserver054/event-payload-939508609.html#Eventpayload-Opened">Eventpayload-Opened</a> * @since Bitbucket Server 5.4
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPullRequestHookProcessor.java+10 −3 modified@@ -126,16 +126,23 @@ protected Map<SCMHead, SCMRevision> heads(@NonNull BitbucketSCMSource source) { 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, source.getRepoOwner(), - source.getRepository(), originalBranchName, pullRequest, headOrigin, strategy); + 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 PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), targetHash), new AbstractGitSCMSource.SCMRevisionImpl(head, pullHash))); break;
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPushHookProcessor.java+60 −27 modified@@ -34,6 +34,8 @@ 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; @@ -78,25 +80,39 @@ public void process(HookEventType hookEvent, String payload, BitbucketType insta return; } - final NativeServerRefsChangedEvent refsChangedEvent; + final BitbucketServerRepository repository; + final List<NativeServerChange> changes; + final String mirrorId; try { - refsChangedEvent = JsonParser.toJava(payload, NativeServerRefsChangedEvent.class); + if (hookEvent == HookEventType.SERVER_REFS_CHANGED) { + final NativeServerRefsChangedEvent event = JsonParser.toJava(payload, NativeServerRefsChangedEvent.class); + repository = event.getRepository(); + changes = event.getChanges(); + mirrorId = null; + } else if (hookEvent == HookEventType.SERVER_MIRROR_REPO_SYNCHRONIZED) { + final NativeServerMirrorRepoSynchronizedEvent event = JsonParser.toJava(payload, NativeServerMirrorRepoSynchronizedEvent.class); + repository = event.getRepository(); + changes = event.getChanges(); + mirrorId = event.getMirrorServer().getId(); + } else { + throw new UnsupportedOperationException("Unsupported hook event " + hookEvent); + } } catch (final IOException e) { LOGGER.log(Level.SEVERE, "Can not read hook payload", e); return; } - final String owner = refsChangedEvent.getRepository().getOwnerName(); - final String repository = refsChangedEvent.getRepository().getRepositoryName(); - if (refsChangedEvent.getChanges().isEmpty()) { + if (changes.isEmpty()) { + final String owner = repository.getOwnerName(); + final String repositoryName = repository.getRepositoryName(); LOGGER.log(Level.INFO, "Received hook from Bitbucket. Processing push event on {0}/{1}", - new Object[] { owner, repository }); - scmSourceReIndex(owner, repository); + new Object[] { owner, repositoryName }); + scmSourceReIndex(owner, repositoryName); return; } - final Multimap<SCMEvent.Type, NativeServerRefsChangedEvent.Change> events = HashMultimap.create(); - for (final NativeServerRefsChangedEvent.Change change : refsChangedEvent.getChanges()) { + final Multimap<SCMEvent.Type, NativeServerChange> events = HashMultimap.create(); + for (final NativeServerChange change : changes) { final String type = change.getType(); if ("UPDATE".equals(type)) { events.put(SCMEvent.Type.UPDATED, change); @@ -110,23 +126,26 @@ public void process(HookEventType hookEvent, String payload, BitbucketType insta } for (final SCMEvent.Type type : events.keySet()) { - SCMHeadEvent.fireLater(new HeadEvent(serverUrl, type, events.get(type), origin, refsChangedEvent), BitbucketSCMSource.getEventDelaySeconds(), TimeUnit.SECONDS); + HeadEvent headEvent = new HeadEvent(serverUrl, type, events.get(type), origin, repository, mirrorId); + SCMHeadEvent.fireLater(headEvent, BitbucketSCMSource.getEventDelaySeconds(), TimeUnit.SECONDS); } } - private static final class HeadEvent extends NativeServerHeadEvent<Collection<NativeServerRefsChangedEvent.Change>> implements HasPullRequests { - private final NativeServerRefsChangedEvent refsChangedEvent; + private static final class HeadEvent extends NativeServerHeadEvent<Collection<NativeServerChange>> implements HasPullRequests { + private final BitbucketServerRepository repository; private final Map<CacheKey, Map<String, BitbucketServerPullRequest>> cachedPullRequests = new HashMap<>(); + private final String mirrorId; - HeadEvent(String serverUrl, Type type, Collection<NativeServerRefsChangedEvent.Change> payload, String origin, - NativeServerRefsChangedEvent refsChangedEvent) { + HeadEvent(String serverUrl, Type type, Collection<NativeServerChange> payload, String origin, + BitbucketServerRepository repository, String mirrorId) { super(serverUrl, type, payload, origin); - this.refsChangedEvent = refsChangedEvent; + this.repository = repository; + this.mirrorId = mirrorId; } @Override protected BitbucketServerRepository getRepository() { - return refsChangedEvent.getRepository(); + return repository; } @Override @@ -146,7 +165,7 @@ protected Map<SCMHead, SCMRevision> heads(BitbucketSCMSource source) { } private void addBranchesAndTags(BitbucketSCMSource src, Map<SCMHead, SCMRevision> result) { - for (final NativeServerRefsChangedEvent.Change change : getPayload()) { + for (final NativeServerChange change : getPayload()) { String refType = change.getRef().getType(); if ("BRANCH".equals(refType)) { @@ -179,12 +198,12 @@ private void addPullRequests(BitbucketSCMSource src, Map<SCMHead, SCMRevision> r final String sourceOwnerName = src.getRepoOwner(); final String sourceRepoName = src.getRepository(); - final BitbucketServerRepository eventRepo = refsChangedEvent.getRepository(); + final BitbucketServerRepository eventRepo = repository; final SCMHeadOrigin headOrigin = src.originOf(eventRepo.getOwnerName(), eventRepo.getRepositoryName()); final Set<ChangeRequestCheckoutStrategy> strategies = headOrigin == SCMHeadOrigin.DEFAULT ? ctx.originPRStrategies() : ctx.forkPRStrategies(); - for (final NativeServerRefsChangedEvent.Change change : getPayload()) { + 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() }); @@ -209,22 +228,30 @@ private void addPullRequests(BitbucketSCMSource src, Map<SCMHead, SCMRevision> r final String branchName = String.format("PR-%s%s", pullRequest.getId(), strategies.size() > 1 ? "-" + Ascii.toLowerCase(strategy.name()) : ""); - final PullRequestSCMHead head = new PullRequestSCMHead(branchName, sourceOwnerName, - sourceRepoName, originalBranchName, pullRequest, headOrigin, strategy); + 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 PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), targetHash), new AbstractGitSCMSource.SCMRevisionImpl(head, pullHash))); } } } } - private Map<String, BitbucketServerPullRequest> getPullRequests(BitbucketSCMSource src, NativeServerRefsChangedEvent.Change change) + private Map<String, BitbucketServerPullRequest> getPullRequests(BitbucketSCMSource src, NativeServerChange change) throws InterruptedException { Map<String, BitbucketServerPullRequest> pullRequests; @@ -240,9 +267,9 @@ private Map<String, BitbucketServerPullRequest> getPullRequests(BitbucketSCMSour } private Map<String, BitbucketServerPullRequest> loadPullRequests(BitbucketSCMSource src, - NativeServerRefsChangedEvent.Change change) throws InterruptedException { + NativeServerChange change) throws InterruptedException { - final BitbucketServerRepository eventRepo = refsChangedEvent.getRepository(); + final BitbucketServerRepository eventRepo = repository; final BitbucketServerAPIClient api = (BitbucketServerAPIClient) src .buildBitbucketClient(eventRepo.getOwnerName(), eventRepo.getRepositoryName()); @@ -278,13 +305,19 @@ private Map<String, BitbucketServerPullRequest> loadPullRequests(BitbucketSCMSou @Override public Collection<BitbucketPullRequest> getPullRequests(BitbucketSCMSource src) throws InterruptedException { List<BitbucketPullRequest> prs = new ArrayList<>(); - for (final NativeServerRefsChangedEvent.Change change : getPayload()) { + for (final NativeServerChange change : getPayload()) { Map<String, BitbucketServerPullRequest> 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 { @@ -293,7 +326,7 @@ private static final class CacheKey { @CheckForNull private final String credentialsId; - CacheKey(BitbucketSCMSource src, NativeServerRefsChangedEvent.Change change) { + CacheKey(BitbucketSCMSource src, NativeServerChange change) { this.refId = requireNonNull(change.getRefId()); this.credentialsId = src.getCredentialsId(); }
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PullRequestHookProcessor.java+10 −23 modified@@ -212,36 +212,23 @@ public Map<SCMHead, SCMRevision> heads(@NonNull SCMSource source) { branchName = branchName + "-" + strategy.name().toLowerCase(Locale.ENGLISH); } String originalBranchName = pull.getSource().getBranch().getName(); - PullRequestSCMHead head; - if (instanceType == BitbucketType.CLOUD) { - head = new PullRequestSCMHead( - branchName, - pullRepoOwner, - pullRepository, - originalBranchName, - pull, - headOrigin, - strategy - ); - } else { - head = new PullRequestSCMHead( - branchName, - src.getRepoOwner(), - src.getRepository(), - originalBranchName, - pull, - headOrigin, - strategy - ); - } + 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, + SCMRevision revision = new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), targetHash), new AbstractGitSCMSource.SCMRevisionImpl(head, pullHash) );
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfiguration.java+9 −0 modified@@ -69,10 +69,17 @@ public class WebhookConfiguration { // only on v5.10 and above HookEventType.SERVER_PULL_REQUEST_MODIFIED.getKey(), HookEventType.SERVER_PULL_REQUEST_REVIEWER_UPDATED.getKey(), + // only on v6.5 and above + HookEventType.SERVER_MIRROR_REPO_SYNCHRONIZED.getKey(), // only on v7.x and above HookEventType.SERVER_PULL_REQUEST_FROM_REF_UPDATED.getKey() )); + /** + * The list of events available in Bitbucket Server v6.5+. + */ + private static final List<String> NATIVE_SERVER_EVENTS_v6_5 = Collections.unmodifiableList(NATIVE_SERVER_EVENTS_v7.subList(0, 8)); + /** * The list of events available in Bitbucket Server v6.x. Applies to v5.10+. */ @@ -222,6 +229,8 @@ private static List<String> getNativeServerEvents(String serverUrl) { // for it to have its // own list return NATIVE_SERVER_EVENTS_v6; + case VERSION_6_5: + return NATIVE_SERVER_EVENTS_v6_5; case VERSION_7: default: return NATIVE_SERVER_EVENTS_v7;
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/MirrorListSupplier.java+27 −0 added@@ -0,0 +1,27 @@ +package com.cloudbees.jenkins.plugins.bitbucket; + +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketMirrorServer; +import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerAPIClient; +import hudson.util.ListBoxModel; +import java.io.IOException; +import java.util.List; + +public class MirrorListSupplier implements BitbucketApiUtils.BitbucketSupplier<ListBoxModel> { + + public static final MirrorListSupplier INSTANCE = new MirrorListSupplier(); + + @Override + public ListBoxModel get(BitbucketApi bitbucketApi) throws IOException, InterruptedException { + ListBoxModel result = new ListBoxModel(new ListBoxModel.Option("Primary server", "")); + if (bitbucketApi instanceof BitbucketServerAPIClient) { + BitbucketServerAPIClient bitbucketServerAPIClient = (BitbucketServerAPIClient) bitbucketApi; + List<BitbucketMirrorServer> mirrors = bitbucketServerAPIClient.getMirrors(); + for (BitbucketMirrorServer mirror : mirrors) { + result.add(new ListBoxModel.Option(mirror.getName(), mirror.getId())); + } + } + return result; + + } +}
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PullRequestSCMHead.java+1 −1 modified@@ -252,7 +252,7 @@ public PullRequestSCMHead migrate(@NonNull BitbucketSCMSource source, @NonNull F public SCMRevision migrate(@NonNull BitbucketSCMSource source, @NonNull AbstractGitSCMSource.SCMRevisionImpl revision) { PullRequestSCMHead head = migrate(source, (FixLegacy) revision.getHead()); - return head != null ? new PullRequestSCMRevision<>(head, + return head != null ? new PullRequestSCMRevision(head, // ChangeRequestCheckoutStrategy.HEAD means we ignore the target revision, // so we can leave it null as a placeholder new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), null),
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PullRequestSCMRevision.java+9 −5 modified@@ -25,7 +25,7 @@ package com.cloudbees.jenkins.plugins.bitbucket; import edu.umd.cs.findbugs.annotations.NonNull; -import jenkins.scm.api.SCMRevision; +import jenkins.plugins.git.AbstractGitSCMSource; import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; import jenkins.scm.api.mixin.ChangeRequestSCMRevision; @@ -34,7 +34,7 @@ * * @since 2.2.0 */ -public class PullRequestSCMRevision<R extends SCMRevision> extends ChangeRequestSCMRevision<PullRequestSCMHead> { +public class PullRequestSCMRevision extends ChangeRequestSCMRevision<PullRequestSCMHead> { /** * Standardize serialization. @@ -45,7 +45,7 @@ public class PullRequestSCMRevision<R extends SCMRevision> extends ChangeRequest * The pull head revision. */ @NonNull - private final R pull; + private final AbstractGitSCMSource.SCMRevisionImpl pull; /** * Constructor. @@ -54,7 +54,7 @@ public class PullRequestSCMRevision<R extends SCMRevision> extends ChangeRequest * @param target the target revision. * @param pull the pull revision. */ - public PullRequestSCMRevision(@NonNull PullRequestSCMHead head, @NonNull R target, @NonNull R pull) { + public PullRequestSCMRevision(@NonNull PullRequestSCMHead head, @NonNull AbstractGitSCMSource.SCMRevisionImpl target, @NonNull AbstractGitSCMSource.SCMRevisionImpl pull) { super(head, target); this.pull = pull; } @@ -65,7 +65,7 @@ public PullRequestSCMRevision(@NonNull PullRequestSCMHead head, @NonNull R targe * @return the pull revision. */ @NonNull - public R getPull() { + public AbstractGitSCMSource.SCMRevisionImpl getPull() { return pull; } @@ -81,6 +81,10 @@ public boolean equivalent(ChangeRequestSCMRevision<?> o) { return getHead().equals(other.getHead()) && pull.equals(other.pull); } + public AbstractGitSCMSource.SCMRevisionImpl getTargetImpl() { + return (AbstractGitSCMSource.SCMRevisionImpl) getTarget(); + } + /** * {@inheritDoc} */
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SCMHeadWithOwnerAndRepo.java+1 −1 modified@@ -160,7 +160,7 @@ public PullRequestSCMHead migrate(@NonNull BitbucketSCMSource source, @NonNull P public SCMRevision migrate(@NonNull BitbucketSCMSource source, @NonNull AbstractGitSCMSource.SCMRevisionImpl revision) { PullRequestSCMHead head = migrate(source, (PR) revision.getHead()); - return head != null ? new PullRequestSCMRevision<>(head, + return head != null ? new PullRequestSCMRevision(head, // ChangeRequestCheckoutStrategy.HEAD means we ignore the target revision, // so we can leave it null as a placeholder new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), null),
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/BitbucketServerVersion.java+2 −1 modified@@ -27,7 +27,8 @@ public enum BitbucketServerVersion implements ModelObject { VERSION_7("Bitbucket v7.x (and later)"), - VERSION_6("Bitbucket v6.x"), + VERSION_6_5("Bitbucket v6.5 to v6.10"), + VERSION_6("Bitbucket v6.0 to v6.4"), VERSION_5_10("Bitbucket v5.10 to v5.16"), VERSION_5("Bitbucket v5.9 (and earlier)");
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java+63 −54 modified@@ -28,9 +28,11 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketCommit; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketMirrorServer; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketMirroredRepository; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketMirroredRepositoryDescriptor; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryProtocol; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRequestException; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketTeam; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook; @@ -45,6 +47,8 @@ import com.cloudbees.jenkins.plugins.bitbucket.server.client.branch.BitbucketServerBranch; import com.cloudbees.jenkins.plugins.bitbucket.server.client.branch.BitbucketServerBranches; import com.cloudbees.jenkins.plugins.bitbucket.server.client.branch.BitbucketServerCommit; +import com.cloudbees.jenkins.plugins.bitbucket.server.client.mirror.BitbucketMirrorServerDescriptors; +import com.cloudbees.jenkins.plugins.bitbucket.server.client.mirror.BitbucketMirroredRepositoryDescriptors; import com.cloudbees.jenkins.plugins.bitbucket.server.client.pullrequest.BitbucketServerPullRequest; import com.cloudbees.jenkins.plugins.bitbucket.server.client.pullrequest.BitbucketServerPullRequestCanMerge; import com.cloudbees.jenkins.plugins.bitbucket.server.client.pullrequest.BitbucketServerPullRequests; @@ -72,7 +76,6 @@ import java.net.InetSocketAddress; import java.net.Proxy; import java.net.URI; -import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; @@ -81,7 +84,6 @@ import java.util.Comparator; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.concurrent.Callable; import java.util.function.Predicate; import java.util.logging.Level; @@ -158,6 +160,10 @@ public class BitbucketServerAPIClient implements BitbucketApi { private static final String WEBHOOK_REPOSITORY_CONFIG_PATH = WEBHOOK_REPOSITORY_PATH + "/{id}"; private static final String API_COMMIT_STATUS_PATH = "/rest/build-status/1.0/commits{/hash}"; + + private static final String API_MIRRORS_FOR_REPO_PATH = "/rest/mirroring/1.0/repos/{id}/mirrors"; + private static final String API_MIRRORS_PATH = "/rest/mirroring/1.0/mirrorServers"; + private static final Integer DEFAULT_PAGE_LIMIT = 200; private static final int API_RATE_LIMIT_STATUS_CODE = 429; private static final Duration API_RATE_LIMIT_INITIAL_SLEEP = Main.isUnitTest ? Duration.ofMillis(100) : Duration.ofSeconds(5); @@ -245,55 +251,6 @@ public String getRepositoryName() { return repositoryName; } - /** - * {@inheritDoc} - */ - @NonNull - @Override - public String getRepositoryUri(@NonNull BitbucketRepositoryProtocol protocol, - @CheckForNull String cloneLink, - @NonNull String owner, - @NonNull String repository) { - URI baseUri; - try { - baseUri = new URI(baseURL); - } catch (URISyntaxException e) { - throw new IllegalStateException("Server URL is not a valid URI", e); - } - - UriTemplate template = UriTemplate.fromTemplate("{scheme}://{+authority}{+path}{/owner,repository}.git"); - template.set("owner", owner); - template.set("repository", repository); - - switch (protocol) { - case HTTP: - template.set("scheme", baseUri.getScheme()); - template.set("authority", baseUri.getRawAuthority()); - template.set("path", Objects.toString(baseUri.getRawPath(), "") + "/scm"); - break; - case SSH: - template.set("scheme", BitbucketRepositoryProtocol.SSH.getType()); - template.set("authority", "git@" + baseUri.getHost()); - if (cloneLink != null) { - try { - URI cloneLinkUri = new URI(cloneLink); - if (cloneLinkUri.getScheme() != null) { - template.set("scheme", cloneLinkUri.getScheme()); - } - if (cloneLinkUri.getRawAuthority() != null) { - template.set("authority", cloneLinkUri.getRawAuthority()); - } - } catch (@SuppressWarnings("unused") URISyntaxException ignored) { - // fall through - } - } - break; - default: - throw new IllegalArgumentException("Unsupported repository protocol: " + protocol); - } - return template.expand(); - } - /** * {@inheritDoc} */ @@ -499,6 +456,54 @@ public BitbucketRepository getRepository() throws IOException, InterruptedExcept } } + /** + * Returns the mirror servers. + * + * @return the mirror servers + * @throws IOException if there was a network communications error. + * @throws InterruptedException if interrupted while waiting on remote communications. + */ + @NonNull + public List<BitbucketMirrorServer> getMirrors() throws IOException, InterruptedException { + UriTemplate uriTemplate = UriTemplate + .fromTemplate(API_MIRRORS_PATH); + return getResources(uriTemplate, BitbucketMirrorServerDescriptors.class); + } + + /** + * Returns the repository mirror descriptors. + * + * @return the repository mirror descriptors for given repository id. + * @throws IOException if there was a network communications error. + * @throws InterruptedException if interrupted while waiting on remote communications. + */ + @NonNull + public List<BitbucketMirroredRepositoryDescriptor> getMirrors(@NonNull Long repositoryId) throws IOException, InterruptedException { + UriTemplate uriTemplate = UriTemplate + .fromTemplate(API_MIRRORS_FOR_REPO_PATH) + .set("id", repositoryId); + return getResources(uriTemplate, BitbucketMirroredRepositoryDescriptors.class); + } + + /** + * Retrieves all available clone urls for the specified repository. + * + * @param url mirror repository self-url + * @return all available clone urls for the specified repository. + * @throws IOException if there was a network communications error. + * @throws InterruptedException if interrupted while waiting on remote communications. + */ + @NonNull + public BitbucketMirroredRepository getMirroredRepository(@NonNull String url) throws IOException, InterruptedException { + HttpGet httpget = new HttpGet(url); + var response = getRequest(httpget); + try { + return JsonParser.toJava(response, BitbucketMirroredRepository.class); + } catch (IOException e) { + throw new IOException("I/O error when accessing URL: " + url, e); + } + } + /** * {@inheritDoc} */ @@ -945,6 +950,10 @@ private <V> V getResource(UriTemplate template, Class<? extends PagedApiResponse protected String getRequest(String path) throws IOException, InterruptedException { HttpGet httpget = new HttpGet(this.baseURL + path); + return getRequest(httpget); + } + + private String getRequest(HttpGet httpget) throws IOException, InterruptedException { if (authenticator != null) { authenticator.configureRequest(httpget); @@ -970,7 +979,7 @@ protected String getRequest(String path) throws IOException, InterruptedExceptio } EntityUtils.consume(response.getEntity()); if (response.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_FOUND) { - throw new FileNotFoundException("URL: " + path); + throw new FileNotFoundException("Request: " + httpget); } if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { throw new BitbucketRequestException(response.getStatusLine().getStatusCode(), @@ -981,7 +990,7 @@ protected String getRequest(String path) throws IOException, InterruptedExceptio } catch (BitbucketRequestException | FileNotFoundException e) { throw e; } catch (IOException e) { - throw new IOException("Communication error for url: " + path, e); + throw new IOException("Communication error for request: " + httpget, e); } finally { httpget.releaseConnection(); }
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/mirror/BitbucketMirroredRepositoryDescriptors.java+7 −0 added@@ -0,0 +1,7 @@ +package com.cloudbees.jenkins.plugins.bitbucket.server.client.mirror; + +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketMirroredRepositoryDescriptor; +import com.cloudbees.jenkins.plugins.bitbucket.server.client.PagedApiResponse; + +public class BitbucketMirroredRepositoryDescriptors extends PagedApiResponse<BitbucketMirroredRepositoryDescriptor> { +}
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/mirror/BitbucketMirrorServerDescriptors.java+7 −0 added@@ -0,0 +1,7 @@ +package com.cloudbees.jenkins.plugins.bitbucket.server.client.mirror; + +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketMirrorServer; +import com.cloudbees.jenkins.plugins.bitbucket.server.client.PagedApiResponse; + +public class BitbucketMirrorServerDescriptors extends PagedApiResponse<BitbucketMirrorServer> { +}
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/BitbucketServerMirrorServer.java+21 −0 added@@ -0,0 +1,21 @@ +package com.cloudbees.jenkins.plugins.bitbucket.server.events; + +public class BitbucketServerMirrorServer { + private String id, name; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +}
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/NativeServerChange.java+46 −0 added@@ -0,0 +1,46 @@ +package com.cloudbees.jenkins.plugins.bitbucket.server.events; + +public class NativeServerChange { + private NativeServerRef ref; + private String refId, fromHash, toHash, type; + + public NativeServerRef getRef() { + return ref; + } + + public void setRef(NativeServerRef ref) { + this.ref = ref; + } + + public String getRefId() { + return refId; + } + + public void setRefId(String refId) { + this.refId = refId; + } + + public String getFromHash() { + return fromHash; + } + + public void setFromHash(String fromHash) { + this.fromHash = fromHash; + } + + public String getToHash() { + return toHash; + } + + public void setToHash(String toHash) { + this.toHash = toHash; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +}
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/NativeServerMirrorRepoSynchronizedEvent.java+25 −0 added@@ -0,0 +1,25 @@ +package com.cloudbees.jenkins.plugins.bitbucket.server.events; + +import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerRepository; +import java.util.Collections; +import java.util.List; + +public class NativeServerMirrorRepoSynchronizedEvent { + private BitbucketServerMirrorServer mirrorServer; + + private BitbucketServerRepository repository; + private List<NativeServerChange> changes; + + public BitbucketServerMirrorServer getMirrorServer() { + return mirrorServer; + } + + public BitbucketServerRepository getRepository() { + return repository; + } + + public List<NativeServerChange> getChanges() { + return changes == null ? Collections.emptyList() : Collections.unmodifiableList(changes); + } + +}
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/NativeServerRef.java+29 −0 added@@ -0,0 +1,29 @@ +package com.cloudbees.jenkins.plugins.bitbucket.server.events; + +public class NativeServerRef { + private String id, displayId, type; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getDisplayId() { + return displayId; + } + + public void setDisplayId(String displayId) { + this.displayId = displayId; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +}
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/NativeServerRefsChangedEvent.java+3 −75 modified@@ -30,86 +30,14 @@ public class NativeServerRefsChangedEvent { private BitbucketServerRepository repository; - private List<Change> changes; + private List<NativeServerChange> changes; public BitbucketServerRepository getRepository() { return repository; } - public List<Change> getChanges() { - return changes == null ? Collections.<Change> emptyList() : Collections.unmodifiableList(changes); - } - - public static class Change { - private Ref ref; - private String refId, fromHash, toHash, type; - - public Ref getRef() { - return ref; - } - - public void setRef(Ref ref) { - this.ref = ref; - } - - public String getRefId() { - return refId; - } - - public void setRefId(String refId) { - this.refId = refId; + public List<NativeServerChange> getChanges() { + return changes == null ? Collections.<NativeServerChange> emptyList() : Collections.unmodifiableList(changes); } - public String getFromHash() { - return fromHash; - } - - public void setFromHash(String fromHash) { - this.fromHash = fromHash; - } - - public String getToHash() { - return toHash; - } - - public void setToHash(String toHash) { - this.toHash = toHash; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - } - - public static class Ref { - private String id, displayId, type; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getDisplayId() { - return displayId; - } - - public void setDisplayId(String displayId) { - this.displayId = displayId; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - } }
src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/config.jelly+3 −0 modified@@ -22,6 +22,9 @@ <f:entry title="${%Project Key}" field="projectKey"> <f:textbox/> </f:entry> + <f:entry title="${%Clone from}" field="mirrorId"> + <f:select/> + </f:entry> <f:entry title="${%Behaviours}" field="traits"> <scm:traits field="traits"/> </f:entry>
src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-mirrorId.html+3 −0 added@@ -0,0 +1,3 @@ +<div> + The location Jenkins should clone from. This can be the primary server or a mirror if one is available. +</div>
src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/config-detail.jelly+3 −0 modified@@ -12,6 +12,9 @@ <f:entry title="${%Repository Name}" field="repository"> <f:select/> </f:entry> + <f:entry title="${%Clone from}" field="mirrorId"> + <f:select/> + </f:entry> <f:entry title="${%Behaviours}" field="traits"> <scm:traits field="traits"/> </f:entry>
src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-mirrorId.html+3 −0 added@@ -0,0 +1,3 @@ +<div> + The location Jenkins should clone from. This can be the primary server or a mirror if one is available. +</div>
src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint/help-webhookImplementation.html+1 −0 modified@@ -3,6 +3,7 @@ <dl> <dt>Plugin</dt> <dd>The third party Webhook implementation provided by a plugin.</dd> + <dd>Please note cloning from mirror is not supported with this implementation.</dd> <dt>Native</dt> <dd>The native Webhook implementation available since Bitbucket Server 5.4.</dd>
src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketBuildStatusNotificationsTest.java+0 −7 modified@@ -29,7 +29,6 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketCommit; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketHref; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryProtocol; import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.filesystem.BitbucketSCMFile; import hudson.model.Action; @@ -68,7 +67,6 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; @@ -117,11 +115,6 @@ private WorkflowMultiBranchProject prepareFirstCheckoutCompletedInvisibleActionT when(api.resolveCommit(sampleRepo.head())).thenReturn(commit); when(commit.getDateMillis()).thenReturn(System.currentTimeMillis()); when(api.checkPathExists(Mockito.anyString(), eq(jenkinsfile))).thenReturn(true); - when(api.getRepositoryUri(any(BitbucketRepositoryProtocol.class), - anyString(), - eq(repoOwner), - eq(repositoryName))) - .thenReturn(sampleRepo.fileUrl()); BitbucketRepository repository = Mockito.mock(BitbucketRepository.class); when(api.getRepository()).thenReturn(repository); when(repository.getOwnerName()).thenReturn(repoOwner);
src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketClientMockUtils.java+14 −10 modified@@ -25,7 +25,6 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketHref; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryProtocol; import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudApiClient; import com.cloudbees.jenkins.plugins.bitbucket.client.branch.BitbucketCloudAuthor; import com.cloudbees.jenkins.plugins.bitbucket.client.branch.BitbucketCloudBranch; @@ -46,9 +45,7 @@ import java.util.List; import jenkins.model.Jenkins; -import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -57,8 +54,6 @@ public class BitbucketClientMockUtils { public static BitbucketCloudApiClient getAPIClientMock(boolean includePullRequests, boolean includeWebHooks) throws IOException, InterruptedException { BitbucketCloudApiClient bitbucket = mock(BitbucketCloudApiClient.class); - when(bitbucket.getRepositoryUri(any(BitbucketRepositoryProtocol.class), nullable(String.class), - anyString(), anyString())).thenCallRealMethod(); // mock branches BitbucketCloudBranch branch1 = getBranch("branch1", "52fc8e220d77ec400f7fc96a91d2fd0bb1bc553a"); @@ -122,7 +117,7 @@ private static List<BitbucketCloudRepository> getRepositories() { new BitbucketHref("https://api.bitbucket.org/2.0/repositories/amuniz/repo1") )); links.put("clone", Arrays.asList( - new BitbucketHref("https","https://bitbucket.org/amuniz/repo1.git"), + new BitbucketHref("http","https://bitbucket.org/amuniz/repo1.git"), new BitbucketHref("ssh","ssh://git@bitbucket.org/amuniz/repo1.git") )); r1.setLinks(links); @@ -133,10 +128,10 @@ private static List<BitbucketCloudRepository> getRepositories() { new BitbucketHref("https://api.bitbucket.org/2.0/repositories/amuniz/repo2") )); links.put("clone", Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/amuniz/repo2.git"), + new BitbucketHref("http", "https://bitbucket.org/amuniz/repo2.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/amuniz/repo2.git") )); - r1.setLinks(links); + r2.setLinks(links); BitbucketCloudRepository r3 = new BitbucketCloudRepository(); // test mock hack to avoid a lot of harness code r3.setFullName("amuniz/test-repos"); @@ -145,10 +140,10 @@ private static List<BitbucketCloudRepository> getRepositories() { new BitbucketHref("https://api.bitbucket.org/2.0/repositories/amuniz/test-repos") )); links.put("clone", Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/amuniz/test-repos.git"), + new BitbucketHref("http", "https://bitbucket.org/amuniz/test-repos.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/amuniz/test-repos.git") )); - r1.setLinks(links); + r3.setLinks(links); return Arrays.asList(r1, r2, r3); } @@ -164,6 +159,15 @@ private static void withMockGitRepos(BitbucketApi bitbucket) throws IOException, repo.setScm("git"); repo.setFullName("amuniz/test-repos"); repo.setPrivate(true); + HashMap<String, List<BitbucketHref>> links = new HashMap<>(); + links.put("self", Collections.singletonList( + new BitbucketHref("https://api.bitbucket.org/2.0/repositories/amuniz/test-repos") + )); + links.put("clone", Arrays.asList( + new BitbucketHref("http", "https://bitbucket.org/amuniz/test-repos.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/amuniz/test-repos.git") + )); + repo.setLinks(links); when(bitbucket.getRepository()).thenReturn(repo); }
src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilderTest.java+1022 −541 modifiedsrc/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMRevisionTest.java+2 −2 modified@@ -99,8 +99,8 @@ public void verify_revision_informations_are_valued() throws Exception { assertRevision(revision); } else if (head instanceof PullRequestSCMHead) { @SuppressWarnings("unchecked") - PullRequestSCMRevision<BitbucketGitSCMRevision> revision = (PullRequestSCMRevision<BitbucketGitSCMRevision>) source.retrieve(head, listener); - assertRevision(revision.getPull()); + PullRequestSCMRevision revision = (PullRequestSCMRevision) source.retrieve(head, listener); + assertRevision((BitbucketGitSCMRevision) revision.getPull()); assertRevision((BitbucketGitSCMRevision) revision.getTarget()); } else if(head instanceof TagSCMHead) { BitbucketTagSCMRevision revision = (BitbucketTagSCMRevision) source.retrieve(head, listener);
src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchScanningTest.java+13 −13 modified@@ -23,6 +23,7 @@ */ package com.cloudbees.jenkins.plugins.bitbucket; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketHref; import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudApiClient; import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint; import edu.umd.cs.findbugs.annotations.NonNull; @@ -42,7 +43,6 @@ import jenkins.scm.api.SCMSourceCriteria; import jenkins.scm.api.SCMSourceOwner; import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; -import org.hamcrest.Matchers; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; @@ -74,18 +74,18 @@ public void clearMockFactory() { public void uriResolverTest() throws Exception { // When there is no checkout credentials set, https must be resolved - assertThat(new BitbucketGitSCMBuilder(getBitbucketSCMSourceMock(false), - new BranchSCMHead("branch1"), null, - null).withBitbucketRemote().remote(), is("https://bitbucket.org/amuniz/test-repos.git")); - } - - @Test - public void remoteConfigsTest() throws Exception { - BitbucketSCMSource source = getBitbucketSCMSourceMock(false); - BitbucketGitSCMBuilder builder = - new BitbucketGitSCMBuilder(source, new BranchSCMHead("branch1"), null, - null); - assertThat(builder.refSpecs(), Matchers.contains("+refs/heads/branch1:refs/remotes/@{remote}/branch1")); + BitbucketGitSCMBuilder builder = new BitbucketGitSCMBuilder( + getBitbucketSCMSourceMock(false), + new BranchSCMHead("branch1"), null, + null + ).withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/amuniz/test-repos.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/amuniz/test-repo.git") + ), + List.of() + ); + assertThat(builder.remote(), is("https://bitbucket.org/amuniz/test-repos.git")); } @Test
src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfigurationTest.java+13 −3 modified@@ -650,7 +650,7 @@ public void should_support_configuration_as_code() throws Exception { BitbucketEndpointConfiguration instance = BitbucketEndpointConfiguration.get(); - assertThat(instance.getEndpoints(), hasSize(11)); + assertThat(instance.getEndpoints(), hasSize(12)); BitbucketCloudEndpoint endpoint1 = (BitbucketCloudEndpoint) instance.getEndpoints().get(0); assertThat(endpoint1.getDisplayName(), is(Messages.BitbucketCloudEndpoint_displayName())); @@ -740,7 +740,7 @@ public void should_support_configuration_as_code() throws Exception { assertThat(serverEndpoint.isCallCanMerge(), is(false)); assertThat(serverEndpoint.isCallChanges(), is(true)); assertThat(serverEndpoint.getWebhookImplementation(), is(BitbucketServerWebhookImplementation.PLUGIN)); - assertThat(serverEndpoint.getServerVersion(), is(BitbucketServerVersion.VERSION_6)); + assertThat(serverEndpoint.getServerVersion(), is(BitbucketServerVersion.VERSION_6_5)); serverEndpoint = (BitbucketServerEndpoint) instance.getEndpoints().get(9); assertThat(serverEndpoint.getDisplayName(), is("Example Inc")); @@ -750,7 +750,7 @@ public void should_support_configuration_as_code() throws Exception { assertThat(serverEndpoint.isCallCanMerge(), is(false)); assertThat(serverEndpoint.isCallChanges(), is(true)); assertThat(serverEndpoint.getWebhookImplementation(), is(BitbucketServerWebhookImplementation.PLUGIN)); - assertThat(serverEndpoint.getServerVersion(), is(BitbucketServerVersion.VERSION_5_10)); + assertThat(serverEndpoint.getServerVersion(), is(BitbucketServerVersion.VERSION_6)); serverEndpoint = (BitbucketServerEndpoint) instance.getEndpoints().get(10); assertThat(serverEndpoint.getDisplayName(), is("Example Inc")); @@ -760,6 +760,16 @@ public void should_support_configuration_as_code() throws Exception { assertThat(serverEndpoint.isCallCanMerge(), is(false)); assertThat(serverEndpoint.isCallChanges(), is(true)); assertThat(serverEndpoint.getWebhookImplementation(), is(BitbucketServerWebhookImplementation.PLUGIN)); + assertThat(serverEndpoint.getServerVersion(), is(BitbucketServerVersion.VERSION_5_10)); + + serverEndpoint = (BitbucketServerEndpoint) instance.getEndpoints().get(11); + assertThat(serverEndpoint.getDisplayName(), is("Example Inc")); + assertThat(serverEndpoint.getServerUrl(), is("http://bitbucket.example.com:8091")); + assertThat(serverEndpoint.isManageHooks(), is(false)); + assertThat(serverEndpoint.getCredentialsId(), is(nullValue())); + assertThat(serverEndpoint.isCallCanMerge(), is(false)); + assertThat(serverEndpoint.isCallChanges(), is(true)); + assertThat(serverEndpoint.getWebhookImplementation(), is(BitbucketServerWebhookImplementation.PLUGIN)); assertThat(serverEndpoint.getServerVersion(), is(BitbucketServerVersion.VERSION_5)); }
src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfigurationTest.java+13 −0 modified@@ -36,6 +36,19 @@ public void given_instanceWithServerVersion6_when_getHooks_SERVER_PR_RVWR_UPDATE assertTrue(hook.getEvents().contains(HookEventType.SERVER_PULL_REQUEST_REVIEWER_UPDATED.getKey())); } + @Test + public void given_instanceWithServerVersion6_5_when_getHooks_SERVER_MIRROR_REPO_SYNC_EVENT_exists() { + WebhookConfiguration whc = new WebhookConfiguration(); + BitbucketSCMSource owner = Mockito.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())); + } + @Test public void given_instanceWithServerVersion510_when_getHooks_SERVER_PR_RVWR_UPDATE_EVENT_exists() { WebhookConfiguration whc = new WebhookConfiguration();
src/test/java/com/cloudbees/jenkins/plugins/bitbucket/UriResolverTest.java+0 −100 removed@@ -1,100 +0,0 @@ -/* - * 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; - -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryProtocol; -import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudApiClient; -import com.cloudbees.jenkins.plugins.bitbucket.server.BitbucketServerWebhookImplementation; -import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerAPIClient; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class UriResolverTest { - - @Test - public void httpUriResolver() throws Exception { - BitbucketApi api = new BitbucketCloudApiClient(false, 0, 0, "test", null, null, (BitbucketAuthenticator) null); - assertEquals("https://bitbucket.org/user1/repo1.git", api.getRepositoryUri( - BitbucketRepositoryProtocol.HTTP, - null, - "user1", - "repo1" - )); - api = new BitbucketServerAPIClient("http://localhost:1234", "test", null, null, false, - BitbucketServerWebhookImplementation.PLUGIN); - assertEquals("http://localhost:1234/scm/user2/repo2.git", api.getRepositoryUri( - BitbucketRepositoryProtocol.HTTP, - null, - "user2", - "repo2" - )); - api = new BitbucketServerAPIClient("http://192.168.1.100:1234", "test", null, null, false, - BitbucketServerWebhookImplementation.PLUGIN); - assertEquals("http://192.168.1.100:1234/scm/user2/repo2.git", api.getRepositoryUri( - BitbucketRepositoryProtocol.HTTP, - null, - "user2", - "repo2" - )); - api = new BitbucketServerAPIClient("http://devtools.test:1234/git/web/", "test", null, null, false, - BitbucketServerWebhookImplementation.PLUGIN); - assertEquals("http://devtools.test:1234/git/web/scm/user2/repo2.git", api.getRepositoryUri( - BitbucketRepositoryProtocol.HTTP, - null, - "user2", - "repo2" - )); - } - - @Test - public void sshUriResolver() throws Exception { - BitbucketApi api = new BitbucketCloudApiClient(false, 0, 0, "test", null, null, (BitbucketAuthenticator) null); - assertEquals("git@bitbucket.org:user1/repo1.git", api.getRepositoryUri( - BitbucketRepositoryProtocol.SSH, - null, - "user1", - "repo1" - )); - api = new BitbucketServerAPIClient("http://localhost:1234", "test", null, null, false, - BitbucketServerWebhookImplementation.PLUGIN); - assertEquals("ssh://git@localhost:7999/user2/repo2.git", api.getRepositoryUri( - BitbucketRepositoryProtocol.SSH, - "ssh://git@localhost:7999/user1/repo1.git", - "user2", - "repo2" - )); - api = new BitbucketServerAPIClient("http://myserver", "test", null, null, false, - BitbucketServerWebhookImplementation.PLUGIN); - assertEquals("ssh://git@myserver:7999/user2/repo2.git", api.getRepositoryUri( - BitbucketRepositoryProtocol.SSH, - "ssh://git@myserver:7999/user1/repo1.git", - "user2", - "repo2" - )); - } - -}
src/test/java/integration/ScanningFailuresTest.java+14 −15 modified@@ -8,8 +8,8 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBranch; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketCommit; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketHref; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryProtocol; import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint; import hudson.model.Result; import hudson.model.TopLevelItem; @@ -18,6 +18,7 @@ import java.util.Collections; import java.util.EnumSet; import java.util.List; +import java.util.Map; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; @@ -44,14 +45,20 @@ import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; @Issue("JENKINS-36029") public class ScanningFailuresTest { + private static final Map<String, List<BitbucketHref>> REPOSITORY_LINKS = Map.of( + "clone", + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + ) + ); + @ClassRule public static JenkinsRule j = new JenkinsRule(); private static final Random entropy = new Random(); @@ -114,14 +121,12 @@ private void getBranchesFails(Callable<Throwable> exception, Result expectedResu when(api.checkPathExists(Mockito.anyString(), eq("Jenkinsfile"))).thenReturn(true); - when(api.getRepositoryUri(any(BitbucketRepositoryProtocol.class), - anyString(), eq("bob"), eq("foo"))).thenReturn(sampleRepo.fileUrl()); - BitbucketRepository repository = Mockito.mock(BitbucketRepository.class); when(api.getRepository()).thenReturn(repository); when(repository.getOwnerName()).thenReturn("bob"); when(repository.getRepositoryName()).thenReturn("foo"); when(repository.getScm()).thenReturn("git"); + when(repository.getLinks()).thenReturn(REPOSITORY_LINKS); BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, api); WorkflowMultiBranchProject mp = j.jenkins.createProject(WorkflowMultiBranchProject.class, "smokes"); @@ -184,14 +189,12 @@ public void checkPathExistsFails() throws Exception { when(api.checkPathExists(Mockito.anyString(), eq("Jenkinsfile"))).thenReturn(true); - when(api.getRepositoryUri(any(BitbucketRepositoryProtocol.class), - anyString(), eq("bob"), eq("foo"))).thenReturn(sampleRepo.fileUrl()); - BitbucketRepository repository = Mockito.mock(BitbucketRepository.class); when(api.getRepository()).thenReturn(repository); when(repository.getOwnerName()).thenReturn("bob"); when(repository.getRepositoryName()).thenReturn("foo"); when(repository.getScm()).thenReturn("git"); + when(repository.getLinks()).thenReturn(REPOSITORY_LINKS); BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, api); WorkflowMultiBranchProject mp = j.jenkins.createProject(WorkflowMultiBranchProject.class, "smokes"); @@ -246,14 +249,12 @@ public void resolveCommitFails() throws Exception { when(api.checkPathExists(Mockito.anyString(), eq("Jenkinsfile"))).thenReturn(true); - when(api.getRepositoryUri(any(BitbucketRepositoryProtocol.class), - anyString(), eq("bob"), eq("foo"))).thenReturn(sampleRepo.fileUrl()); - BitbucketRepository repository = Mockito.mock(BitbucketRepository.class); when(api.getRepository()).thenReturn(repository); when(repository.getOwnerName()).thenReturn("bob"); when(repository.getRepositoryName()).thenReturn("foo"); when(repository.getScm()).thenReturn("git"); + when(repository.getLinks()).thenReturn(REPOSITORY_LINKS); BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, api); WorkflowMultiBranchProject mp = j.jenkins.createProject(WorkflowMultiBranchProject.class, "smokes"); @@ -311,14 +312,12 @@ public void branchRemoved() throws Exception { when(api.checkPathExists(Mockito.anyString(), eq("Jenkinsfile"))).thenReturn(true); - when(api.getRepositoryUri(any(BitbucketRepositoryProtocol.class), - anyString(), eq("bob"), eq("foo"))).thenReturn(sampleRepo.fileUrl()); - BitbucketRepository repository = Mockito.mock(BitbucketRepository.class); when(api.getRepository()).thenReturn(repository); when(repository.getOwnerName()).thenReturn("bob"); when(repository.getRepositoryName()).thenReturn("foo"); when(repository.getScm()).thenReturn("git"); + when(repository.getLinks()).thenReturn(REPOSITORY_LINKS); BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, api); WorkflowMultiBranchProject mp = j.jenkins.createProject(WorkflowMultiBranchProject.class, "smokes");
src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfigurationTest/configuration-as-code.yml+8 −2 modified@@ -57,16 +57,22 @@ unclassified: serverUrl: "http://bitbucket.example.com:8088" callCanMerge: false callChanges: true - serverVersion: VERSION_6 + serverVersion: VERSION_6_5 - bitbucketServerEndpoint: displayName: "Example Inc" serverUrl: "http://bitbucket.example.com:8089" callCanMerge: false callChanges: true - serverVersion: VERSION_5_10 + serverVersion: VERSION_6 - bitbucketServerEndpoint: displayName: "Example Inc" serverUrl: "http://bitbucket.example.com:8090" callCanMerge: false callChanges: true + serverVersion: VERSION_5_10 + - bitbucketServerEndpoint: + displayName: "Example Inc" + serverUrl: "http://bitbucket.example.com:8091" + callCanMerge: false + callChanges: true serverVersion: VERSION_5
src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfigurationTest/configuration-as-code.yml+7 −0 modified@@ -9,6 +9,13 @@ unclassified: callChanges: false serverVersion: VERSION_7 webhookImplementation: NATIVE + - bitbucketServerEndpoint: + displayName: "Example Inc" + serverUrl: "http://bitbucket.example.com:8091" + callCanMerge: false + callChanges: true + serverVersion: VERSION_6_5 + webhookImplementation: NATIVE - bitbucketServerEndpoint: displayName: "Example Inc" serverUrl: "http://bitbucket.example.com:8088"
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-m4rm-x2rr-357wghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-28152ghsaADVISORY
- www.jenkins.io/security/advisory/2024-03-06/ghsavendor-advisoryWEB
- www.openwall.com/lists/oss-security/2024/03/06/3ghsaWEB
- github.com/jenkinsci/bitbucket-branch-source-plugin/commit/28d74e8b4226bfc7524b412e34f7090784cc1a08ghsaWEB
News mentions
1- Jenkins Security Advisory 2024-03-06Jenkins Security Advisories · Mar 6, 2024