From 28d74e8b4226bfc7524b412e34f7090784cc1a08 Mon Sep 17 00:00:00 2001 From: Andrey Fomin Date: Sat, 10 Feb 2024 14:29:05 +0300 Subject: [PATCH] Support cloning from Bitbucket mirror (#796) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 PullRequestSCMHead doesn't contain the hash needed for the build and SCMRevision is null. This is the current design limitation and can be refactored later. 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. Fixes #592 Co-authors: - Andrey Fomin https://github.com/andrey-fomin - Andrei Kouznetchik https://github.com/akouznetchik - Eugene Mercuriev https://github.com/zamonier Co-authored-by: Günter Grodotzki --- docs/USER_GUIDE.adoc | 21 +- docs/images/screenshot-13.png | Bin 0 -> 238367 bytes .../plugins/bitbucket/BitbucketApiUtils.java | 93 + .../bitbucket/BitbucketGitSCMBuilder.java | 226 ++- .../bitbucket/BitbucketSCMNavigator.java | 41 +- .../plugins/bitbucket/BitbucketSCMSource.java | 285 +-- .../bitbucket/BitbucketSCMSourceBuilder.java | 19 +- .../plugins/bitbucket/BranchWithHash.java | 19 + ...lbackToOtherRepositoryGitSCMExtension.java | 77 + .../plugins/bitbucket/MirrorListSupplier.java | 27 + .../plugins/bitbucket/PullRequestSCMHead.java | 2 +- .../bitbucket/PullRequestSCMRevision.java | 14 +- .../bitbucket/SCMHeadWithOwnerAndRepo.java | 2 +- .../plugins/bitbucket/api/BitbucketApi.java | 15 - .../bitbucket/api/BitbucketMirrorServer.java | 38 + .../api/BitbucketMirroredRepository.java | 28 + ...BitbucketMirroredRepositoryDescriptor.java | 30 + .../client/BitbucketCloudApiClient.java | 27 - .../bitbucket/hooks/HookEventType.java | 6 + .../NativeServerPullRequestHookProcessor.java | 13 +- .../hooks/NativeServerPushHookProcessor.java | 87 +- .../hooks/PullRequestHookProcessor.java | 33 +- .../bitbucket/hooks/WebhookConfiguration.java | 9 + .../server/BitbucketServerVersion.java | 3 +- .../client/BitbucketServerAPIClient.java | 117 +- .../BitbucketMirrorServerDescriptors.java | 7 + ...itbucketMirroredRepositoryDescriptors.java | 7 + .../events/BitbucketServerMirrorServer.java | 21 + .../server/events/NativeServerChange.java | 46 + ...tiveServerMirrorRepoSynchronizedEvent.java | 25 + .../server/events/NativeServerRef.java | 29 + .../events/NativeServerRefsChangedEvent.java | 78 +- .../BitbucketSCMNavigator/config.jelly | 3 + .../BitbucketSCMNavigator/help-mirrorId.html | 3 + .../BitbucketSCMSource/config-detail.jelly | 3 + .../BitbucketSCMSource/help-mirrorId.html | 3 + .../help-webhookImplementation.html | 1 + ...BitbucketBuildStatusNotificationsTest.java | 7 - .../bitbucket/BitbucketClientMockUtils.java | 24 +- .../bitbucket/BitbucketGitSCMBuilderTest.java | 1563 +++++++++++------ .../BitbucketGitSCMRevisionTest.java | 4 +- .../plugins/bitbucket/BranchScanningTest.java | 26 +- .../plugins/bitbucket/UriResolverTest.java | 100 -- .../BitbucketEndpointConfigurationTest.java | 16 +- .../hooks/WebhookConfigurationTest.java | 13 + .../integration/ScanningFailuresTest.java | 29 +- .../configuration-as-code.yml | 10 +- .../configuration-as-code.yml | 7 + 48 files changed, 2120 insertions(+), 1137 deletions(-) create mode 100644 docs/images/screenshot-13.png create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketApiUtils.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BranchWithHash.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/FallbackToOtherRepositoryGitSCMExtension.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/MirrorListSupplier.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketMirrorServer.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketMirroredRepository.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketMirroredRepositoryDescriptor.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/mirror/BitbucketMirrorServerDescriptors.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/mirror/BitbucketMirroredRepositoryDescriptors.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/BitbucketServerMirrorServer.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/NativeServerChange.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/NativeServerMirrorRepoSynchronizedEvent.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/NativeServerRef.java create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-mirrorId.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-mirrorId.html delete mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/UriResolverTest.java diff --git a/docs/USER_GUIDE.adoc b/docs/USER_GUIDE.adoc index 198eeab74..3ac023659 100644 --- a/docs/USER_GUIDE.adoc +++ b/docs/USER_GUIDE.adoc @@ -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 diff --git a/docs/images/screenshot-13.png b/docs/images/screenshot-13.png new file mode 100644 index 0000000000000000000000000000000000000000..a475ccfd0e9cd7c2eb74ed17ec8a905b77d6fd72 GIT binary patch literal 238367 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 AGynhq literal 0 HcmV?d00001 diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketApiUtils.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketApiUtils.java new file mode 100644 index 000000000..f33cfca52 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketApiUtils.java @@ -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 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 get(BitbucketApi bitbucketApi) throws IOException, InterruptedException; + } + +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilder.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilder.java index eafab07d0..46c00e610 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilder.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilder.java @@ -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 cloneLinks = Collections.emptyList(); + private List primaryCloneLinks = List.of(); + + /** + * The clone links for mirror repository if it's configured + */ + @NonNull + private List 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 cloneLinks) { - this.cloneLinks = new ArrayList<>(Util.fixNull(cloneLinks)); + public BitbucketGitSCMBuilder withCloneLinks( + @CheckForNull List primaryCloneLinks, + @CheckForNull List 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 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 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 cloneLinks) { + return cloneLinks.stream() + .filter(link -> protocol.getType().equals(link.getName())) + .findAny() + .orElseThrow(() -> new IllegalStateException("Can't find clone link for protocol " + protocol)) + .getHref(); } /** diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java index fd531123b..a542fcb83 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java @@ -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>> 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(); } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java index 07428ec60..0700f3a1e 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java @@ -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 pullRequestContributorCache; /** - * The cache of the clone links. + * The cache of the primary clone links. */ @CheckForNull - private transient List cloneLinks = null; + private transient List primaryCloneLinks = null; + /** + * The cache of the mirror clone links. + */ + @CheckForNull + private transient List 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> 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> 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> 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> 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 retrieveActions(@CheckForNull SCMSourceEvent event, BitbucketRepository r = bitbucket.getRepository(); Map> 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 getCloneLinksFromMirror( + BitbucketServerAPIClient bitbucket, + String mirrorIdLocal + ) throws IOException, InterruptedException { + // Mirrors are supported only by Bitbucket Server + BitbucketServerRepository r = (BitbucketServerRepository) bitbucket.getRepository(); + List 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> mirrorDescriptorLinks = mirroredRepositoryDescriptor.getLinks(); + if (mirrorDescriptorLinks == null) { + throw new IllegalStateException("There is no repository descriptor links for mirror id " + mirrorIdLocal); + } + List 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> mirroredRepositoryLinks = mirroredRepository.getLinks(); + if (mirroredRepositoryLinks == null) { + throw new IllegalStateException("There is no mirrored repository links for mirror id " + mirrorIdLocal); + } + List 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 getCloneLinksFromPrimary(BitbucketApi bitbucket) throws IOException, InterruptedException { + BitbucketRepository r = bitbucket.getRepository(); + Map> links = r.getLinks(); + if (links == null) { + throw new IllegalStateException("There is no links"); + } + List 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,40 +1381,11 @@ 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 listBoxModelSupplier = bitbucket -> { + ListBoxModel result = new ListBoxModel(); BitbucketTeam team = bitbucket.getTeam(); List 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(); } @@ -1332,29 +1393,21 @@ public ListBoxModel doFillRepositoryItems(@AncestorInPath SCMSourceOwner context 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)); diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceBuilder.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceBuilder.java index e8fc04e68..cd080dbf2 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceBuilder.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceBuilder.java @@ -49,6 +49,11 @@ public class BitbucketSCMSourceBuilder extends SCMSourceBuilder branchWithHashes; + + public FallbackToOtherRepositoryGitSCMExtension( + String cloneLink, + String remoteName, + List 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 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); + } + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/MirrorListSupplier.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/MirrorListSupplier.java new file mode 100644 index 000000000..020f7f667 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/MirrorListSupplier.java @@ -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 { + + 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 mirrors = bitbucketServerAPIClient.getMirrors(); + for (BitbucketMirrorServer mirror : mirrors) { + result.add(new ListBoxModel.Option(mirror.getName(), mirror.getId())); + } + } + return result; + + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PullRequestSCMHead.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PullRequestSCMHead.java index 00ccd37cf..d3a3162af 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PullRequestSCMHead.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PullRequestSCMHead.java @@ -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), diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PullRequestSCMRevision.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PullRequestSCMRevision.java index 15e4185a8..7831f1269 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PullRequestSCMRevision.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PullRequestSCMRevision.java @@ -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 extends ChangeRequestSCMRevision { +public class PullRequestSCMRevision extends ChangeRequestSCMRevision { /** * Standardize serialization. @@ -45,7 +45,7 @@ public class PullRequestSCMRevision extends ChangeRequest * The pull head revision. */ @NonNull - private final R pull; + private final AbstractGitSCMSource.SCMRevisionImpl pull; /** * Constructor. @@ -54,7 +54,7 @@ public class PullRequestSCMRevision 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} */ diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SCMHeadWithOwnerAndRepo.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SCMHeadWithOwnerAndRepo.java index 1988facc1..b94a51300 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SCMHeadWithOwnerAndRepo.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SCMHeadWithOwnerAndRepo.java @@ -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), diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java index db3df153e..891369d9c 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java @@ -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. * diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketMirrorServer.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketMirrorServer.java new file mode 100644 index 000000000..dfec27c0f --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketMirrorServer.java @@ -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; + } + +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketMirroredRepository.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketMirroredRepository.java new file mode 100644 index 000000000..34909059b --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketMirroredRepository.java @@ -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> links; + + public boolean isAvailable() { + return available; + } + + public void setAvailable(boolean available) { + this.available = available; + } + + public Map> getLinks() { + return links; + } + + public void setLinks(Map> links) { + this.links = links; + } + +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketMirroredRepositoryDescriptor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketMirroredRepositoryDescriptor.java new file mode 100644 index 000000000..61bc0d251 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketMirroredRepositoryDescriptor.java @@ -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> links; + + public BitbucketMirrorServer getMirrorServer() { + return mirrorServer; + } + + public void setMirrorServer(BitbucketMirrorServer mirrorServer) { + this.mirrorServer = mirrorServer; + } + + public Map> getLinks() { + return links; + } + + public void setLinks(Map> links) { + this.links = links; + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java index cf7d23f29..4612f4d95 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java @@ -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} */ diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/HookEventType.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/HookEventType.java index 4983d8903..734781947 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/HookEventType.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/HookEventType.java @@ -67,6 +67,12 @@ public enum HookEventType { */ SERVER_REFS_CHANGED("repo:refs_changed", NativeServerPushHookProcessor.class), + /** + * @see Eventpayload-repo-mirr-syn + * @since Bitbucket Server 6.5 + */ + SERVER_MIRROR_REPO_SYNCHRONIZED("mirror:repo_synchronized", NativeServerPushHookProcessor.class), + /** * @see Eventpayload-Opened * @since Bitbucket Server 5.4 diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPullRequestHookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPullRequestHookProcessor.java index 67061ec59..11f6b1ceb 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPullRequestHookProcessor.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPullRequestHookProcessor.java @@ -126,8 +126,15 @@ protected Map 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: @@ -135,7 +142,7 @@ protected Map heads(@NonNull BitbucketSCMSource source) { 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; diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPushHookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPushHookProcessor.java index e523dec9f..0c4cf27de 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPushHookProcessor.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/NativeServerPushHookProcessor.java @@ -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 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 events = HashMultimap.create(); - for (final NativeServerRefsChangedEvent.Change change : refsChangedEvent.getChanges()) { + final Multimap 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> implements HasPullRequests { - private final NativeServerRefsChangedEvent refsChangedEvent; + private static final class HeadEvent extends NativeServerHeadEvent> implements HasPullRequests { + private final BitbucketServerRepository repository; private final Map> cachedPullRequests = new HashMap<>(); + private final String mirrorId; - HeadEvent(String serverUrl, Type type, Collection payload, String origin, - NativeServerRefsChangedEvent refsChangedEvent) { + HeadEvent(String serverUrl, Type type, Collection 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 heads(BitbucketSCMSource source) { } private void addBranchesAndTags(BitbucketSCMSource src, Map 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 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 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,14 +228,22 @@ private void addPullRequests(BitbucketSCMSource src, Map 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))); } @@ -224,7 +251,7 @@ private void addPullRequests(BitbucketSCMSource src, Map r } } - private Map getPullRequests(BitbucketSCMSource src, NativeServerRefsChangedEvent.Change change) + private Map getPullRequests(BitbucketSCMSource src, NativeServerChange change) throws InterruptedException { Map pullRequests; @@ -240,9 +267,9 @@ private Map getPullRequests(BitbucketSCMSour } private Map 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 loadPullRequests(BitbucketSCMSou @Override public Collection getPullRequests(BitbucketSCMSource src) throws InterruptedException { List prs = new ArrayList<>(); - for (final NativeServerRefsChangedEvent.Change change : getPayload()) { + for (final NativeServerChange change : getPayload()) { Map prsForChange = getPullRequests(src, change); prs.addAll(prsForChange.values()); } return prs; } + + @Override + protected boolean eventMatchesRepo(BitbucketSCMSource source) { + return Objects.equals(source.getMirrorId(), this.mirrorId) && super.eventMatchesRepo(source); + } + } private static final class CacheKey { @@ -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(); } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PullRequestHookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PullRequestHookProcessor.java index ae1789fad..f146e1970 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PullRequestHookProcessor.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PullRequestHookProcessor.java @@ -212,28 +212,15 @@ public Map 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); @@ -241,7 +228,7 @@ public Map heads(@NonNull SCMSource source) { 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) ); diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfiguration.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfiguration.java index a673c1866..bbd14c4de 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfiguration.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfiguration.java @@ -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 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 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; diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/BitbucketServerVersion.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/BitbucketServerVersion.java index 399a70e0d..a09381adf 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/BitbucketServerVersion.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/BitbucketServerVersion.java @@ -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)"); diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java index 4cda7bed2..93f0c4c45 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java @@ -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 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 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 getResource(UriTemplate template, Class { +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/mirror/BitbucketMirroredRepositoryDescriptors.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/mirror/BitbucketMirroredRepositoryDescriptors.java new file mode 100644 index 000000000..5d12dfaa7 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/mirror/BitbucketMirroredRepositoryDescriptors.java @@ -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 { +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/BitbucketServerMirrorServer.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/BitbucketServerMirrorServer.java new file mode 100644 index 000000000..52619a275 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/BitbucketServerMirrorServer.java @@ -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; + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/NativeServerChange.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/NativeServerChange.java new file mode 100644 index 000000000..0f6d537d6 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/NativeServerChange.java @@ -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; + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/NativeServerMirrorRepoSynchronizedEvent.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/NativeServerMirrorRepoSynchronizedEvent.java new file mode 100644 index 000000000..7a7e372e9 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/NativeServerMirrorRepoSynchronizedEvent.java @@ -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 changes; + + public BitbucketServerMirrorServer getMirrorServer() { + return mirrorServer; + } + + public BitbucketServerRepository getRepository() { + return repository; + } + + public List getChanges() { + return changes == null ? Collections.emptyList() : Collections.unmodifiableList(changes); + } + +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/NativeServerRef.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/NativeServerRef.java new file mode 100644 index 000000000..60144649b --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/NativeServerRef.java @@ -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; + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/NativeServerRefsChangedEvent.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/NativeServerRefsChangedEvent.java index e4547e173..f28e204a7 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/NativeServerRefsChangedEvent.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/events/NativeServerRefsChangedEvent.java @@ -30,86 +30,14 @@ public class NativeServerRefsChangedEvent { private BitbucketServerRepository repository; - private List changes; + private List changes; public BitbucketServerRepository getRepository() { return repository; } - public List getChanges() { - return changes == null ? Collections. 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 getChanges() { + return changes == null ? Collections. 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; - } - } } diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/config.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/config.jelly index e988fa82b..d4d66073d 100644 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/config.jelly +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/config.jelly @@ -22,6 +22,9 @@ + + + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-mirrorId.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-mirrorId.html new file mode 100644 index 000000000..1b72ccdf3 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-mirrorId.html @@ -0,0 +1,3 @@ +

+ The location Jenkins should clone from. This can be the primary server or a mirror if one is available. +
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/config-detail.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/config-detail.jelly index 5906d9cc3..4daa73707 100644 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/config-detail.jelly +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/config-detail.jelly @@ -12,6 +12,9 @@ + + + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-mirrorId.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-mirrorId.html new file mode 100644 index 000000000..1b72ccdf3 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-mirrorId.html @@ -0,0 +1,3 @@ +
+ The location Jenkins should clone from. This can be the primary server or a mirror if one is available. +
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint/help-webhookImplementation.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint/help-webhookImplementation.html index 4e6f493d0..94916270f 100644 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint/help-webhookImplementation.html +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint/help-webhookImplementation.html @@ -3,6 +3,7 @@
Plugin
The third party Webhook implementation provided by a plugin.
+
Please note cloning from mirror is not supported with this implementation.
Native
The native Webhook implementation available since Bitbucket Server 5.4.
diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketBuildStatusNotificationsTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketBuildStatusNotificationsTest.java index 49a4af20a..8bd4d9507 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketBuildStatusNotificationsTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketBuildStatusNotificationsTest.java @@ -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); diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketClientMockUtils.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketClientMockUtils.java index e358c0410..626ba96ef 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketClientMockUtils.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketClientMockUtils.java @@ -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 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 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 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> 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); } diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilderTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilderTest.java index 0e7b3831c..9f47279ec 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilderTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilderTest.java @@ -72,6 +72,7 @@ public void tearDown() throws IOException, InterruptedException { SystemCredentialsProvider.getInstance() .setDomainCredentialsMap(Collections.>emptyMap()); owner.delete(); + BitbucketMockApiFactory.clear(); } @Test @@ -85,18 +86,20 @@ public void given__cloud_branch_rev_anon__when__build__then__scmBuilt() throws E assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.org/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); @@ -143,18 +146,20 @@ public void given__cloud_branch_rev_userpass__when__build__then__scmBuilt() thro assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.org/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); @@ -201,18 +206,20 @@ public void given__cloud_branch_rev_userkey__when__build__then__scmBuilt() throw assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); - assertThat(instance.remote(), is("git@bitbucket.org:tester/test-repo.git")); + ), + List.of() + ); + assertThat(instance.remote(), is("ssh://git@bitbucket.org/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); @@ -222,12 +229,12 @@ public void given__cloud_branch_rev_userkey__when__build__then__scmBuilt() throw UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); assertThat(config.getRefspec(), is("+refs/heads/test-branch:refs/remotes/origin/test-branch")); - assertThat(config.getUrl(), is("git@bitbucket.org:tester/test-repo.git")); + assertThat(config.getUrl(), is("ssh://git@bitbucket.org/tester/test-repo.git")); assertThat(config.getCredentialsId(), is("user-key")); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("git@bitbucket.org:tester/test-repo.git")); + assertThat(origin.getURIs().get(0).toString(), is("ssh://git@bitbucket.org/tester/test-repo.git")); assertThat(origin.getFetchRefSpecs(), hasSize(1)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/test-branch")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/test-branch")); @@ -257,18 +264,20 @@ public void given__cloud_branch_norev_anon__when__build__then__scmBuilt() throws assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is(nullValue())); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.org/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); @@ -301,18 +310,20 @@ public void given__cloud_branch_norev_userpass__when__build__then__scmBuilt() th assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is(nullValue())); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.org/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); @@ -345,18 +356,20 @@ public void given__cloud_branch_norev_userkey__when__build__then__scmBuilt() thr assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is(nullValue())); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); - assertThat(instance.remote(), is("git@bitbucket.org:tester/test-repo.git")); + ), + List.of() + ); + assertThat(instance.remote(), is("ssh://git@bitbucket.org/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); @@ -366,12 +379,12 @@ public void given__cloud_branch_norev_userkey__when__build__then__scmBuilt() thr UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); assertThat(config.getRefspec(), is("+refs/heads/test-branch:refs/remotes/origin/test-branch")); - assertThat(config.getUrl(), is("git@bitbucket.org:tester/test-repo.git")); + assertThat(config.getUrl(), is("ssh://git@bitbucket.org/tester/test-repo.git")); assertThat(config.getCredentialsId(), is("user-key")); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("git@bitbucket.org:tester/test-repo.git")); + assertThat(origin.getURIs().get(0).toString(), is("ssh://git@bitbucket.org/tester/test-repo.git")); assertThat(origin.getFetchRefSpecs(), hasSize(1)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/test-branch")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/test-branch")); @@ -392,18 +405,20 @@ public void given__server_branch_rev_anon__when__build__then__scmBuilt() throws assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.test")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); @@ -439,6 +454,69 @@ public void given__server_branch_rev_anon__when__build__then__scmBuilt() throws assertThat(revisions.iterator().next().getSha1String(), is("cafebabedeadbeefcafebabedeadbeefcafebabe")); } + @Test + public void given__server_withMirror_branch_rev_anon__when__build__then__scmBuilt() throws Exception { + source.setServerUrl("https://bitbucket.test"); + BranchSCMHead head = new BranchSCMHead("test-branch"); + AbstractGitSCMSource.SCMRevisionImpl revision = + new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe"); + BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, + head, revision, null); + assertThat(instance.credentialsId(), is(nullValue())); + assertThat(instance.head(), is(head)); + assertThat(instance.revision(), is(revision)); + assertThat(instance.scmSource(), is(source)); + assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", + instance.remote(), is("https://bitbucket.test")); + assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); + assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); + + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") + ), + List.of( + new BitbucketHref("http", "https://bitbucket-mirror.test/scm/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket-mirror.test:7999/tester/test-repo.git") + ) + ); + assertThat(instance.remote(), is("https://bitbucket-mirror.test/scm/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); + assertThat(actual.getGitTool(), nullValue()); + assertThat(actual.getUserRemoteConfigs(), hasSize(1)); + UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); + assertThat(config.getName(), is("origin")); + assertThat(config.getRefspec(), is("+refs/heads/test-branch:refs/remotes/origin/test-branch")); + assertThat(config.getUrl(), is("https://bitbucket-mirror.test/scm/tester/test-repo.git")); + assertThat(config.getCredentialsId(), is(nullValue())); + RemoteConfig origin = actual.getRepositoryByName("origin"); + assertThat(origin, notNullValue()); + assertThat(origin.getURIs(), hasSize(1)); + assertThat(origin.getURIs().get(0).toString(), is("https://bitbucket-mirror.test/scm/tester/test-repo.git")); + assertThat(origin.getFetchRefSpecs(), hasSize(1)); + assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/test-branch")); + assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/test-branch")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), containsInAnyOrder( + instanceOf(GitSCMSourceDefaults.class), + instanceOf(BuildChooserSetting.class) + )); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); + assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); + AbstractGitSCMSource.SpecificRevisionBuildChooser revChooser = + (AbstractGitSCMSource.SpecificRevisionBuildChooser) chooser.getBuildChooser(); + Collection revisions = revChooser + .getCandidateRevisions(false, "test-branch", Mockito.mock(GitClient.class), new LogTaskListener( + Logger.getAnonymousLogger(), Level.FINEST), null, null); + assertThat(revisions, hasSize(1)); + assertThat(revisions.iterator().next().getSha1String(), is("cafebabedeadbeefcafebabedeadbeefcafebabe")); + } @Test public void given__server_branch_rev_userpass__when__build__then__scmBuilt() throws Exception { @@ -452,18 +530,20 @@ public void given__server_branch_rev_userpass__when__build__then__scmBuilt() thr assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.test")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); @@ -511,18 +591,20 @@ public void given__server_branch_rev_userkey__when__build__then__scmBuilt() thro assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.test")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("ssh://git@bitbucket.test:7999/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); @@ -570,19 +652,21 @@ public void given__server_branch_rev_userkey_different_clone_url__when__build__t assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://www.bitbucket.test/web")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://www.bitbucket.test/web/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@web.bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://web.bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); @@ -629,18 +713,20 @@ public void given__server_branch_norev_anon__when__build__then__scmBuilt() throw assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is(nullValue())); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.test")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); @@ -674,18 +760,20 @@ public void given__server_branch_norev_userpass__when__build__then__scmBuilt() t assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is(nullValue())); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.test")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); @@ -719,18 +807,20 @@ public void given__server_branch_norev_userkey__when__build__then__scmBuilt() th assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is(nullValue())); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.test")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("ssh://git@bitbucket.test:7999/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); @@ -764,19 +854,21 @@ public void given__server_branch_norev_userkey_different_clone_url__when__build_ assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is(nullValue())); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://www.bitbucket.test/web")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://www.bitbucket.test/web/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@www.bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://www.bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/test-branch:refs/remotes/@{remote}/test-branch")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); @@ -806,8 +898,8 @@ public void given__cloud_pullHead_rev_anon__when__build__then__scmBuilt() throws PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), ChangeRequestCheckoutStrategy.HEAD); - PullRequestSCMRevision revision = - new PullRequestSCMRevision<>(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), "deadbeefcafebabedeadbeefcafebabedeadbeef"), new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, @@ -816,22 +908,24 @@ public void given__cloud_pullHead_rev_anon__when__build__then__scmBuilt() throws assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.org/qa/qa-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); - assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); assertThat(actual.getGitTool(), nullValue()); assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); @@ -868,8 +962,8 @@ public void given__cloud_pullHead_rev_userpass__when__build__then__scmBuilt() th PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), ChangeRequestCheckoutStrategy.HEAD); - PullRequestSCMRevision revision = - new PullRequestSCMRevision<>(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), "deadbeefcafebabedeadbeefcafebabedeadbeef"), new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, @@ -878,22 +972,24 @@ public void given__cloud_pullHead_rev_userpass__when__build__then__scmBuilt() th assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.org/qa/qa-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); - assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); assertThat(actual.getGitTool(), nullValue()); assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); @@ -930,8 +1026,8 @@ public void given__cloud_pullHead_rev_userkey__when__build__then__scmBuilt() thr PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), ChangeRequestCheckoutStrategy.HEAD); - PullRequestSCMRevision revision = - new PullRequestSCMRevision<>(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), "deadbeefcafebabedeadbeefcafebabedeadbeef"), new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, @@ -940,33 +1036,34 @@ public void given__cloud_pullHead_rev_userkey__when__build__then__scmBuilt() thr assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); - assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); - assertThat(instance.remote(), is("git@bitbucket.org:qa/qa-repo.git")); + ), + List.of() + ); + assertThat(instance.remote(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); - assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); assertThat(actual.getGitTool(), nullValue()); assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); assertThat(config.getRefspec(), is("+refs/heads/qa-branch:refs/remotes/origin/PR-1")); - assertThat(config.getUrl(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(config.getUrl(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(config.getCredentialsId(), is("user-key")); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(origin.getURIs().get(0).toString(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(origin.getFetchRefSpecs(), hasSize(1)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/qa-branch")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); @@ -992,8 +1089,8 @@ public void given__cloud_pullHead_rev_anon_sshtrait_anon__when__build__then__scm PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), ChangeRequestCheckoutStrategy.HEAD); - PullRequestSCMRevision revision = - new PullRequestSCMRevision<>(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), "deadbeefcafebabedeadbeefcafebabedeadbeef"), new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, @@ -1002,39 +1099,41 @@ public void given__cloud_pullHead_rev_anon_sshtrait_anon__when__build__then__scm assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.org/qa/qa-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); SSHCheckoutTrait sshTrait = new SSHCheckoutTrait(null); sshTrait.decorateBuilder(instance); GitSCM actual = instance.build(); assertThat(instance.credentialsId(), is(nullValue())); - assertThat(instance.remote(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(instance.remote(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); - assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); assertThat(actual.getGitTool(), nullValue()); assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); assertThat(config.getRefspec(), is("+refs/heads/qa-branch:refs/remotes/origin/PR-1")); - assertThat(config.getUrl(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(config.getUrl(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(config.getCredentialsId(), is(nullValue())); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(origin.getURIs().get(0).toString(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(origin.getFetchRefSpecs(), hasSize(1)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/qa-branch")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); @@ -1060,8 +1159,8 @@ public void given__cloud_pullHead_rev_userpass_sshtrait_anon__when__build__then_ PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), ChangeRequestCheckoutStrategy.HEAD); - PullRequestSCMRevision revision = - new PullRequestSCMRevision<>(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), "deadbeefcafebabedeadbeefcafebabedeadbeef"), new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, @@ -1070,39 +1169,41 @@ public void given__cloud_pullHead_rev_userpass_sshtrait_anon__when__build__then_ assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.org/qa/qa-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); SSHCheckoutTrait sshTrait = new SSHCheckoutTrait(null); sshTrait.decorateBuilder(instance); GitSCM actual = instance.build(); assertThat(instance.credentialsId(), is(nullValue())); - assertThat(instance.remote(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(instance.remote(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); - assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); assertThat(actual.getGitTool(), nullValue()); assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); assertThat(config.getRefspec(), is("+refs/heads/qa-branch:refs/remotes/origin/PR-1")); - assertThat(config.getUrl(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(config.getUrl(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(config.getCredentialsId(), is(nullValue())); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(origin.getURIs().get(0).toString(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(origin.getFetchRefSpecs(), hasSize(1)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/qa-branch")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); @@ -1128,8 +1229,8 @@ public void given__cloud_pullHead_rev_userkey_sshtrait_anon__when__build__then__ PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), ChangeRequestCheckoutStrategy.HEAD); - PullRequestSCMRevision revision = - new PullRequestSCMRevision<>(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), "deadbeefcafebabedeadbeefcafebabedeadbeef"), new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, @@ -1138,39 +1239,41 @@ public void given__cloud_pullHead_rev_userkey_sshtrait_anon__when__build__then__ assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); - assertThat(instance.remote(), is("git@bitbucket.org:qa/qa-repo.git")); + ), + List.of() + ); + assertThat(instance.remote(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); SSHCheckoutTrait sshTrait = new SSHCheckoutTrait(null); sshTrait.decorateBuilder(instance); GitSCM actual = instance.build(); assertThat(instance.credentialsId(), is(nullValue())); - assertThat(instance.remote(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(instance.remote(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); - assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); assertThat(actual.getGitTool(), nullValue()); assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); assertThat(config.getRefspec(), is("+refs/heads/qa-branch:refs/remotes/origin/PR-1")); - assertThat(config.getUrl(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(config.getUrl(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(config.getCredentialsId(), is(nullValue())); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(origin.getURIs().get(0).toString(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(origin.getFetchRefSpecs(), hasSize(1)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/qa-branch")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); @@ -1196,8 +1299,8 @@ public void given__cloud_pullHead_rev_anon_sshtrait_userkey__when__build__then__ PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), ChangeRequestCheckoutStrategy.HEAD); - PullRequestSCMRevision revision = - new PullRequestSCMRevision<>(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), "deadbeefcafebabedeadbeefcafebabedeadbeef"), new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, @@ -1206,39 +1309,41 @@ public void given__cloud_pullHead_rev_anon_sshtrait_userkey__when__build__then__ assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.org/qa/qa-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); SSHCheckoutTrait sshTrait = new SSHCheckoutTrait("user-key"); sshTrait.decorateBuilder(instance); GitSCM actual = instance.build(); assertThat(instance.credentialsId(), is("user-key")); - assertThat(instance.remote(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(instance.remote(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); - assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); assertThat(actual.getGitTool(), nullValue()); assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); assertThat(config.getRefspec(), is("+refs/heads/qa-branch:refs/remotes/origin/PR-1")); - assertThat(config.getUrl(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(config.getUrl(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(config.getCredentialsId(), is("user-key")); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(origin.getURIs().get(0).toString(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(origin.getFetchRefSpecs(), hasSize(1)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/qa-branch")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); @@ -1264,8 +1369,8 @@ public void given__cloud_pullHead_rev_userpass_sshtrait_userkey__when__build__th PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), ChangeRequestCheckoutStrategy.HEAD); - PullRequestSCMRevision revision = - new PullRequestSCMRevision<>(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), "deadbeefcafebabedeadbeefcafebabedeadbeef"), new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, @@ -1274,39 +1379,41 @@ public void given__cloud_pullHead_rev_userpass_sshtrait_userkey__when__build__th assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.org/qa/qa-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); SSHCheckoutTrait sshTrait = new SSHCheckoutTrait("user-key"); sshTrait.decorateBuilder(instance); GitSCM actual = instance.build(); assertThat(instance.credentialsId(), is("user-key")); - assertThat(instance.remote(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(instance.remote(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); - assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); assertThat(actual.getGitTool(), nullValue()); assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); assertThat(config.getRefspec(), is("+refs/heads/qa-branch:refs/remotes/origin/PR-1")); - assertThat(config.getUrl(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(config.getUrl(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(config.getCredentialsId(), is("user-key")); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(origin.getURIs().get(0).toString(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(origin.getFetchRefSpecs(), hasSize(1)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/qa-branch")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); @@ -1332,8 +1439,8 @@ public void given__cloud_pullHead_rev_userkey_sshtrait_userkey__when__build__the PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), ChangeRequestCheckoutStrategy.HEAD); - PullRequestSCMRevision revision = - new PullRequestSCMRevision<>(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), "deadbeefcafebabedeadbeefcafebabedeadbeef"), new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, @@ -1342,39 +1449,41 @@ public void given__cloud_pullHead_rev_userkey_sshtrait_userkey__when__build__the assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); - assertThat(instance.remote(), is("git@bitbucket.org:qa/qa-repo.git")); + ), + List.of() + ); + assertThat(instance.remote(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); SSHCheckoutTrait sshTrait = new SSHCheckoutTrait("user-key"); sshTrait.decorateBuilder(instance); GitSCM actual = instance.build(); assertThat(instance.credentialsId(), is("user-key")); - assertThat(instance.remote(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(instance.remote(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); - assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); assertThat(actual.getGitTool(), nullValue()); assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); assertThat(config.getRefspec(), is("+refs/heads/qa-branch:refs/remotes/origin/PR-1")); - assertThat(config.getUrl(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(config.getUrl(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(config.getCredentialsId(), is("user-key")); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(origin.getURIs().get(0).toString(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(origin.getFetchRefSpecs(), hasSize(1)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/qa-branch")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); @@ -1406,22 +1515,24 @@ public void given__cloud_pullHead_norev_anon__when__build__then__scmBuilt() thro assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is(nullValue())); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.org/qa/qa-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); - assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); assertThat(actual.getGitTool(), nullValue()); assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); @@ -1452,22 +1563,24 @@ public void given__cloud_pullHead_norev_userpass__when__build__then__scmBuilt() assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is(nullValue())); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.org/qa/qa-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); - assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); assertThat(actual.getGitTool(), nullValue()); assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); @@ -1498,33 +1611,35 @@ public void given__cloud_pullHead_norev_userkey__when__build__then__scmBuilt() t assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is(nullValue())); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); - assertThat(instance.remote(), is("git@bitbucket.org:qa/qa-repo.git")); + ), + List.of() + ); + assertThat(instance.remote(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); - assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); assertThat(actual.getGitTool(), nullValue()); assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); assertThat(config.getRefspec(), is("+refs/heads/qa-branch:refs/remotes/origin/PR-1")); - assertThat(config.getUrl(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(config.getUrl(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(config.getCredentialsId(), is("user-key")); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(origin.getURIs().get(0).toString(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(origin.getFetchRefSpecs(), hasSize(1)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/qa-branch")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); @@ -1539,8 +1654,8 @@ public void given__server_pullHead_rev_anon__when__build__then__scmBuilt() throw PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), ChangeRequestCheckoutStrategy.HEAD); - PullRequestSCMRevision revision = - new PullRequestSCMRevision<>(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), "deadbeefcafebabedeadbeefcafebabedeadbeef"), new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, @@ -1549,18 +1664,20 @@ public void given__server_pullHead_rev_anon__when__build__then__scmBuilt() throw assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.test")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); @@ -1596,35 +1713,39 @@ public void given__server_pullHead_rev_anon__when__build__then__scmBuilt() throw assertThat(revisions.iterator().next().getSha1String(), is("cafebabedeadbeefcafebabedeadbeefcafebabe")); } - @Test - public void given__server_pullHead_rev_userpass__when__build__then__scmBuilt() throws Exception { + public void given__server_withMirror_pullHead_rev_anon__when__build__then__scmBuilt() throws Exception { source.setServerUrl("https://bitbucket.test"); PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), ChangeRequestCheckoutStrategy.HEAD); - PullRequestSCMRevision revision = - new PullRequestSCMRevision<>(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), "deadbeefcafebabedeadbeefcafebabedeadbeef"), new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, - head, revision, "user-pass"); - assertThat(instance.credentialsId(), is("user-pass")); + head, revision, null); + assertThat(instance.credentialsId(), is(nullValue())); assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.test")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of( + new BitbucketHref("http", "https://bitbucket-mirror.test/scm/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket-mirror.test:7999/tester/test-repo.git") + ) + ); assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); @@ -1635,7 +1756,7 @@ public void given__server_pullHead_rev_userpass__when__build__then__scmBuilt() t assertThat(config.getName(), is("origin")); assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1")); assertThat(config.getUrl(), is("https://bitbucket.test/scm/tester/test-repo.git")); - assertThat(config.getCredentialsId(), is("user-pass")); + assertThat(config.getCredentialsId(), is(nullValue())); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); @@ -1661,33 +1782,35 @@ public void given__server_pullHead_rev_userpass__when__build__then__scmBuilt() t } @Test - public void given__server_pullHead_rev_userkey__when__build__then__scmBuilt() throws Exception { + public void given__server_pullHead_defaultOrigin_rev_anon__when__build__then__scmBuilt() throws Exception { source.setServerUrl("https://bitbucket.test"); - PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", - new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "tester", "test-repo", "pr-branch", "1", "a fake title", + new BranchSCMHead("test-branch"), SCMHeadOrigin.DEFAULT, ChangeRequestCheckoutStrategy.HEAD); - PullRequestSCMRevision revision = - new PullRequestSCMRevision<>(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), "deadbeefcafebabedeadbeefcafebabedeadbeef"), new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, - head, revision, "user-key"); - assertThat(instance.credentialsId(), is("user-key")); + head, revision, null); + assertThat(instance.credentialsId(), is(nullValue())); assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.test")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") - )); - assertThat(instance.remote(), is("ssh://git@bitbucket.test:7999/tester/test-repo.git")); + ), + List.of() + ); + assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/pr-branch:refs/remotes/@{remote}/pr-branch")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); @@ -1696,16 +1819,16 @@ public void given__server_pullHead_rev_userkey__when__build__then__scmBuilt() th assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); - assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1")); - assertThat(config.getUrl(), is("ssh://git@bitbucket.test:7999/tester/test-repo.git")); - assertThat(config.getCredentialsId(), is("user-key")); + assertThat(config.getRefspec(), is("+refs/heads/pr-branch:refs/remotes/origin/pr-branch")); + assertThat(config.getUrl(), is("https://bitbucket.test/scm/tester/test-repo.git")); + assertThat(config.getCredentialsId(), is(nullValue())); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("ssh://git@bitbucket.test:7999/tester/test-repo.git")); + assertThat(origin.getURIs().get(0).toString(), is("https://bitbucket.test/scm/tester/test-repo.git")); assertThat(origin.getFetchRefSpecs(), hasSize(1)); - assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/pull-requests/1/from")); - assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); + assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/pr-branch")); + assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/pr-branch")); assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); assertThat(actual.getExtensions(), containsInAnyOrder( @@ -1717,100 +1840,117 @@ public void given__server_pullHead_rev_userkey__when__build__then__scmBuilt() th AbstractGitSCMSource.SpecificRevisionBuildChooser revChooser = (AbstractGitSCMSource.SpecificRevisionBuildChooser) chooser.getBuildChooser(); Collection revisions = revChooser - .getCandidateRevisions(false, "qa-branch", Mockito.mock(GitClient.class), new LogTaskListener( + .getCandidateRevisions(false, "pr-branch", Mockito.mock(GitClient.class), new LogTaskListener( Logger.getAnonymousLogger(), Level.FINEST), null, null); assertThat(revisions, hasSize(1)); assertThat(revisions.iterator().next().getSha1String(), is("cafebabedeadbeefcafebabedeadbeefcafebabe")); } @Test - public void given__server_pullHead_rev_userkey_different_clone_url__when__build__then__scmBuilt() throws Exception { - source.setServerUrl("https://www.bitbucket.test/web"); - PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", - new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), + public void given__server_withMirror_pullHead_defaultOrigin_rev_anon__when__build__then__scmBuilt() throws Exception { + source.setServerUrl("https://bitbucket.test"); + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "tester", "test-repo", "pr-branch", "1", "a fake title", + new BranchSCMHead("test-branch"), SCMHeadOrigin.DEFAULT, ChangeRequestCheckoutStrategy.HEAD); - PullRequestSCMRevision revision = - new PullRequestSCMRevision<>(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), "deadbeefcafebabedeadbeefcafebabedeadbeef"), new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, - head, revision, "user-key"); - assertThat(instance.credentialsId(), is("user-key")); - assertThat(instance.head(), is((SCMHead) head)); - assertThat(instance.revision(), is((SCMRevision) revision)); + head, revision, null); + assertThat(instance.credentialsId(), is(nullValue())); + assertThat(instance.head(), is(head)); + assertThat(instance.revision(), is(revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", - instance.remote(), is("https://www.bitbucket.test/web")); + instance.remote(), is("https://bitbucket.test")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); - assertThat(instance.browser().getRepoUrl(), - is("https://www.bitbucket.test/web/projects/tester/repos/test-repo")); + assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@www.bitbucket.test/scm/tester/test-repo.git"), - new BitbucketHref("ssh", "ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git") - )); - assertThat(instance.remote(), is("ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git")); + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") + ), + List.of( + new BitbucketHref("http", "https://bitbucket-mirror.test/scm/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket-mirror.test:7999/tester/test-repo.git") + ) + ); + assertThat(instance.remote(), is("https://bitbucket-mirror.test/scm/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/pr-branch:refs/remotes/@{remote}/pr-branch")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); - assertThat(actual.getBrowser().getRepoUrl(), - is("https://www.bitbucket.test/web/projects/tester/repos/test-repo")); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); assertThat(actual.getGitTool(), nullValue()); assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); - assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1")); - assertThat(config.getUrl(), is("ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git")); - assertThat(config.getCredentialsId(), is("user-key")); + assertThat(config.getRefspec(), is("+refs/heads/pr-branch:refs/remotes/origin/pr-branch")); + assertThat(config.getUrl(), is("https://bitbucket-mirror.test/scm/tester/test-repo.git")); + assertThat(config.getCredentialsId(), is(nullValue())); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git")); + assertThat(origin.getURIs().get(0).toString(), is("https://bitbucket-mirror.test/scm/tester/test-repo.git")); assertThat(origin.getFetchRefSpecs(), hasSize(1)); - assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/pull-requests/1/from")); - assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); + assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/pr-branch")); + assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/pr-branch")); assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); assertThat(actual.getExtensions(), containsInAnyOrder( instanceOf(BuildChooserSetting.class), - instanceOf(GitSCMSourceDefaults.class)) - ); + instanceOf(GitSCMSourceDefaults.class), + instanceOf(FallbackToOtherRepositoryGitSCMExtension.class) + )); BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); AbstractGitSCMSource.SpecificRevisionBuildChooser revChooser = (AbstractGitSCMSource.SpecificRevisionBuildChooser) chooser.getBuildChooser(); Collection revisions = revChooser - .getCandidateRevisions(false, "qa-branch", Mockito.mock(GitClient.class), new LogTaskListener( + .getCandidateRevisions(false, "pr-branch", Mockito.mock(GitClient.class), new LogTaskListener( Logger.getAnonymousLogger(), Level.FINEST), null, null); assertThat(revisions, hasSize(1)); assertThat(revisions.iterator().next().getSha1String(), is("cafebabedeadbeefcafebabedeadbeefcafebabe")); } + @Test - public void given__server_pullHead_norev_anon__when__build__then__scmBuilt() throws Exception { + public void given__server_pullMerge_defaultOrigin_rev_anon__when__build__then__scmBuilt() throws Exception { source.setServerUrl("https://bitbucket.test"); - PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", - new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), - ChangeRequestCheckoutStrategy.HEAD); + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "tester", "test-repo", "pr-branch", "1", "a fake title", + new BranchSCMHead("test-branch"), SCMHeadOrigin.DEFAULT, + ChangeRequestCheckoutStrategy.MERGE); + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + "deadbeefcafebabedeadbeefcafebabedeadbeef"), + new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, - head, null, null); + head, revision, null); assertThat(instance.credentialsId(), is(nullValue())); assertThat(instance.head(), is((SCMHead) head)); - assertThat(instance.revision(), is(nullValue())); + assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", - instance.remote(), is("https://bitbucket.test")); + instance.remote(), is("https://bitbucket.test")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + assertThat( + instance.refSpecs(), + contains( + "+refs/heads/pr-branch:refs/remotes/@{remote}/pr-branch", + "+refs/heads/test-branch:refs/remotes/@{remote}/test-branch" + ) + ); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); @@ -1819,14 +1959,356 @@ public void given__server_pullHead_norev_anon__when__build__then__scmBuilt() thr assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); - assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1")); + assertThat(config.getRefspec(), is("+refs/heads/pr-branch:refs/remotes/origin/pr-branch +refs/heads/test-branch:refs/remotes/origin/test-branch")); assertThat(config.getUrl(), is("https://bitbucket.test/scm/tester/test-repo.git")); assertThat(config.getCredentialsId(), is(nullValue())); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); assertThat(origin.getURIs().get(0).toString(), is("https://bitbucket.test/scm/tester/test-repo.git")); - assertThat(origin.getFetchRefSpecs(), hasSize(1)); + assertThat(origin.getFetchRefSpecs(), hasSize(2)); + assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/pr-branch")); + assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/pr-branch")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(origin.getFetchRefSpecs().get(1).getSource(), is("refs/heads/test-branch")); + assertThat(origin.getFetchRefSpecs().get(1).getDestination(), is("refs/remotes/origin/test-branch")); + assertThat(origin.getFetchRefSpecs().get(1).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(1).isWildcard(), is(false)); + assertThat(actual.getExtensions(), containsInAnyOrder( + instanceOf(BuildChooserSetting.class), + instanceOf(GitSCMSourceDefaults.class), + instanceOf(MergeWithGitSCMExtension.class) + )); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); + assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); + AbstractGitSCMSource.SpecificRevisionBuildChooser revChooser = + (AbstractGitSCMSource.SpecificRevisionBuildChooser) chooser.getBuildChooser(); + Collection revisions = revChooser + .getCandidateRevisions(false, "pr-branch", Mockito.mock(GitClient.class), new LogTaskListener( + Logger.getAnonymousLogger(), Level.FINEST), null, null); + assertThat(revisions, hasSize(1)); + assertThat(revisions.iterator().next().getSha1String(), is("cafebabedeadbeefcafebabedeadbeefcafebabe")); + } + + @Test + public void given__server_withMirror_pullMerge_defaultOrigin_rev_anon__when__build__then__scmBuilt() throws Exception { + source.setServerUrl("https://bitbucket.test"); + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "tester", "test-repo", "pr-branch", "1", "a fake title", + new BranchSCMHead("test-branch"), SCMHeadOrigin.DEFAULT, + ChangeRequestCheckoutStrategy.MERGE); + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + "deadbeefcafebabedeadbeefcafebabedeadbeef"), + new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); + BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, + head, revision, null); + assertThat(instance.credentialsId(), is(nullValue())); + assertThat(instance.head(), is((SCMHead) head)); + assertThat(instance.revision(), is((SCMRevision) revision)); + assertThat(instance.scmSource(), is(source)); + assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", + instance.remote(), is("https://bitbucket.test")); + assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); + assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); + + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") + ), + List.of( + new BitbucketHref("http", "https://bitbucket-mirror.test/scm/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket-mirror.test:7999/tester/test-repo.git") + ) + ); + assertThat(instance.remote(), is("https://bitbucket-mirror.test/scm/tester/test-repo.git")); + assertThat( + instance.refSpecs(), + contains( + "+refs/heads/pr-branch:refs/remotes/@{remote}/pr-branch", + "+refs/heads/test-branch:refs/remotes/@{remote}/test-branch" + ) + ); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); + assertThat(actual.getGitTool(), nullValue()); + assertThat(actual.getUserRemoteConfigs(), hasSize(1)); + UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); + assertThat(config.getName(), is("origin")); + assertThat(config.getRefspec(), is("+refs/heads/pr-branch:refs/remotes/origin/pr-branch +refs/heads/test-branch:refs/remotes/origin/test-branch")); + assertThat(config.getUrl(), is("https://bitbucket-mirror.test/scm/tester/test-repo.git")); + assertThat(config.getCredentialsId(), is(nullValue())); + RemoteConfig origin = actual.getRepositoryByName("origin"); + assertThat(origin, notNullValue()); + assertThat(origin.getURIs(), hasSize(1)); + assertThat(origin.getURIs().get(0).toString(), is("https://bitbucket-mirror.test/scm/tester/test-repo.git")); + assertThat(origin.getFetchRefSpecs(), hasSize(2)); + assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/pr-branch")); + assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/pr-branch")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(origin.getFetchRefSpecs().get(1).getSource(), is("refs/heads/test-branch")); + assertThat(origin.getFetchRefSpecs().get(1).getDestination(), is("refs/remotes/origin/test-branch")); + assertThat(origin.getFetchRefSpecs().get(1).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(1).isWildcard(), is(false)); + assertThat(actual.getExtensions(), containsInAnyOrder( + instanceOf(BuildChooserSetting.class), + instanceOf(GitSCMSourceDefaults.class), + instanceOf(MergeWithGitSCMExtension.class), + instanceOf(FallbackToOtherRepositoryGitSCMExtension.class) + )); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); + assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); + AbstractGitSCMSource.SpecificRevisionBuildChooser revChooser = + (AbstractGitSCMSource.SpecificRevisionBuildChooser) chooser.getBuildChooser(); + Collection revisions = revChooser + .getCandidateRevisions(false, "pr-branch", Mockito.mock(GitClient.class), new LogTaskListener( + Logger.getAnonymousLogger(), Level.FINEST), null, null); + assertThat(revisions, hasSize(1)); + assertThat(revisions.iterator().next().getSha1String(), is("cafebabedeadbeefcafebabedeadbeefcafebabe")); + } + + @Test + public void given__server_pullHead_rev_userpass__when__build__then__scmBuilt() throws Exception { + source.setServerUrl("https://bitbucket.test"); + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", + new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), + ChangeRequestCheckoutStrategy.HEAD); + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + "deadbeefcafebabedeadbeefcafebabedeadbeef"), + new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); + BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, + head, revision, "user-pass"); + assertThat(instance.credentialsId(), is("user-pass")); + assertThat(instance.head(), is((SCMHead) head)); + assertThat(instance.revision(), is((SCMRevision) revision)); + assertThat(instance.scmSource(), is(source)); + assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", + instance.remote(), is("https://bitbucket.test")); + assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); + assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); + + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") + ), + List.of() + ); + assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); + assertThat(actual.getGitTool(), nullValue()); + assertThat(actual.getUserRemoteConfigs(), hasSize(1)); + UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); + assertThat(config.getName(), is("origin")); + assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1")); + assertThat(config.getUrl(), is("https://bitbucket.test/scm/tester/test-repo.git")); + assertThat(config.getCredentialsId(), is("user-pass")); + RemoteConfig origin = actual.getRepositoryByName("origin"); + assertThat(origin, notNullValue()); + assertThat(origin.getURIs(), hasSize(1)); + assertThat(origin.getURIs().get(0).toString(), is("https://bitbucket.test/scm/tester/test-repo.git")); + assertThat(origin.getFetchRefSpecs(), hasSize(1)); + assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/pull-requests/1/from")); + assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), containsInAnyOrder( + instanceOf(BuildChooserSetting.class), + instanceOf(GitSCMSourceDefaults.class)) + ); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); + assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); + AbstractGitSCMSource.SpecificRevisionBuildChooser revChooser = + (AbstractGitSCMSource.SpecificRevisionBuildChooser) chooser.getBuildChooser(); + Collection revisions = revChooser + .getCandidateRevisions(false, "qa-branch", Mockito.mock(GitClient.class), new LogTaskListener( + Logger.getAnonymousLogger(), Level.FINEST), null, null); + assertThat(revisions, hasSize(1)); + assertThat(revisions.iterator().next().getSha1String(), is("cafebabedeadbeefcafebabedeadbeefcafebabe")); + } + + @Test + public void given__server_pullHead_rev_userkey__when__build__then__scmBuilt() throws Exception { + source.setServerUrl("https://bitbucket.test"); + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", + new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), + ChangeRequestCheckoutStrategy.HEAD); + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + "deadbeefcafebabedeadbeefcafebabedeadbeef"), + new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); + BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, + head, revision, "user-key"); + assertThat(instance.credentialsId(), is("user-key")); + assertThat(instance.head(), is((SCMHead) head)); + assertThat(instance.revision(), is((SCMRevision) revision)); + assertThat(instance.scmSource(), is(source)); + assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", + instance.remote(), is("https://bitbucket.test")); + assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); + assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); + + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") + ), + List.of() + ); + assertThat(instance.remote(), is("ssh://git@bitbucket.test:7999/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); + assertThat(actual.getGitTool(), nullValue()); + assertThat(actual.getUserRemoteConfigs(), hasSize(1)); + UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); + assertThat(config.getName(), is("origin")); + assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1")); + assertThat(config.getUrl(), is("ssh://git@bitbucket.test:7999/tester/test-repo.git")); + assertThat(config.getCredentialsId(), is("user-key")); + RemoteConfig origin = actual.getRepositoryByName("origin"); + assertThat(origin, notNullValue()); + assertThat(origin.getURIs(), hasSize(1)); + assertThat(origin.getURIs().get(0).toString(), is("ssh://git@bitbucket.test:7999/tester/test-repo.git")); + assertThat(origin.getFetchRefSpecs(), hasSize(1)); + assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/pull-requests/1/from")); + assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), containsInAnyOrder( + instanceOf(BuildChooserSetting.class), + instanceOf(GitSCMSourceDefaults.class)) + ); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); + assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); + AbstractGitSCMSource.SpecificRevisionBuildChooser revChooser = + (AbstractGitSCMSource.SpecificRevisionBuildChooser) chooser.getBuildChooser(); + Collection revisions = revChooser + .getCandidateRevisions(false, "qa-branch", Mockito.mock(GitClient.class), new LogTaskListener( + Logger.getAnonymousLogger(), Level.FINEST), null, null); + assertThat(revisions, hasSize(1)); + assertThat(revisions.iterator().next().getSha1String(), is("cafebabedeadbeefcafebabedeadbeefcafebabe")); + } + + @Test + public void given__server_pullHead_rev_userkey_different_clone_url__when__build__then__scmBuilt() throws Exception { + source.setServerUrl("https://www.bitbucket.test/web"); + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", + new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), + ChangeRequestCheckoutStrategy.HEAD); + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + "deadbeefcafebabedeadbeefcafebabedeadbeef"), + new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); + BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, + head, revision, "user-key"); + assertThat(instance.credentialsId(), is("user-key")); + assertThat(instance.head(), is((SCMHead) head)); + assertThat(instance.revision(), is((SCMRevision) revision)); + assertThat(instance.scmSource(), is(source)); + assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", + instance.remote(), is("https://www.bitbucket.test/web")); + assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); + assertThat(instance.browser().getRepoUrl(), + is("https://www.bitbucket.test/web/projects/tester/repos/test-repo")); + + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://www.bitbucket.test/scm/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git") + ), + List.of() + ); + assertThat(instance.remote(), is("ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + assertThat(actual.getBrowser().getRepoUrl(), + is("https://www.bitbucket.test/web/projects/tester/repos/test-repo")); + assertThat(actual.getGitTool(), nullValue()); + assertThat(actual.getUserRemoteConfigs(), hasSize(1)); + UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); + assertThat(config.getName(), is("origin")); + assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1")); + assertThat(config.getUrl(), is("ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git")); + assertThat(config.getCredentialsId(), is("user-key")); + RemoteConfig origin = actual.getRepositoryByName("origin"); + assertThat(origin, notNullValue()); + assertThat(origin.getURIs(), hasSize(1)); + assertThat(origin.getURIs().get(0).toString(), is("ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git")); + assertThat(origin.getFetchRefSpecs(), hasSize(1)); + assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/pull-requests/1/from")); + assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), containsInAnyOrder( + instanceOf(BuildChooserSetting.class), + instanceOf(GitSCMSourceDefaults.class)) + ); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); + assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); + AbstractGitSCMSource.SpecificRevisionBuildChooser revChooser = + (AbstractGitSCMSource.SpecificRevisionBuildChooser) chooser.getBuildChooser(); + Collection revisions = revChooser + .getCandidateRevisions(false, "qa-branch", Mockito.mock(GitClient.class), new LogTaskListener( + Logger.getAnonymousLogger(), Level.FINEST), null, null); + assertThat(revisions, hasSize(1)); + assertThat(revisions.iterator().next().getSha1String(), is("cafebabedeadbeefcafebabedeadbeefcafebabe")); + } + @Test + public void given__server_pullHead_norev_anon__when__build__then__scmBuilt() throws Exception { + source.setServerUrl("https://bitbucket.test"); + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", + new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), + ChangeRequestCheckoutStrategy.HEAD); + BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, + head, null, null); + assertThat(instance.credentialsId(), is(nullValue())); + assertThat(instance.head(), is((SCMHead) head)); + assertThat(instance.revision(), is(nullValue())); + assertThat(instance.scmSource(), is(source)); + assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", + instance.remote(), is("https://bitbucket.test")); + assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); + assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); + + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") + ), + List.of() + ); + assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); + assertThat(actual.getGitTool(), nullValue()); + assertThat(actual.getUserRemoteConfigs(), hasSize(1)); + UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); + assertThat(config.getName(), is("origin")); + assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1")); + assertThat(config.getUrl(), is("https://bitbucket.test/scm/tester/test-repo.git")); + assertThat(config.getCredentialsId(), is(nullValue())); + RemoteConfig origin = actual.getRepositoryByName("origin"); + assertThat(origin, notNullValue()); + assertThat(origin.getURIs(), hasSize(1)); + assertThat(origin.getURIs().get(0).toString(), is("https://bitbucket.test/scm/tester/test-repo.git")); + assertThat(origin.getFetchRefSpecs(), hasSize(1)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/pull-requests/1/from")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); @@ -1846,18 +2328,20 @@ public void given__server_pullHead_norev_userpass__when__build__then__scmBuilt() assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is(nullValue())); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.test")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); @@ -1893,18 +2377,20 @@ public void given__server_pullHead_norev_userkey__when__build__then__scmBuilt() assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is(nullValue())); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.test")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("ssh://git@bitbucket.test:7999/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); @@ -1940,24 +2426,23 @@ public void given__server_pullHead_norev_userkey__when_different_clone_url__buil assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is(nullValue())); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://www.bitbucket.test/web")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://www.bitbucket.test/web/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@www.bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://www.bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); GitSCM actual = instance.build(); - assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); - assertThat(actual.getBrowser().getRepoUrl(), - is("https://www.bitbucket.test/web/projects/tester/repos/test-repo")); assertThat(actual.getGitTool(), nullValue()); assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); @@ -1982,8 +2467,8 @@ public void given__cloud_pullMerge_rev_anon__when__build__then__scmBuilt() throw PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), ChangeRequestCheckoutStrategy.MERGE); - PullRequestSCMRevision revision = - new PullRequestSCMRevision<>(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), "deadbeefcafebabedeadbeefcafebabedeadbeef"), new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, @@ -1992,22 +2477,24 @@ public void given__cloud_pullMerge_rev_anon__when__build__then__scmBuilt() throw assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.org/qa/qa-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); - assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); assertThat(actual.getGitTool(), nullValue()); assertThat(actual.getUserRemoteConfigs(), hasSize(2)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); @@ -2064,8 +2551,8 @@ public void given__cloud_pullMerge_rev_userpass__when__build__then__scmBuilt() t PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), ChangeRequestCheckoutStrategy.MERGE); - PullRequestSCMRevision revision = - new PullRequestSCMRevision<>(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), "deadbeefcafebabedeadbeefcafebabedeadbeef"), new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, @@ -2074,22 +2561,24 @@ public void given__cloud_pullMerge_rev_userpass__when__build__then__scmBuilt() t assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.org/qa/qa-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); - assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); assertThat(actual.getGitTool(), nullValue()); assertThat(actual.getUserRemoteConfigs(), hasSize(2)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); @@ -2146,8 +2635,8 @@ public void given__cloud_pullMerge_rev_userkey__when__build__then__scmBuilt() th PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), ChangeRequestCheckoutStrategy.MERGE); - PullRequestSCMRevision revision = - new PullRequestSCMRevision<>(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), "deadbeefcafebabedeadbeefcafebabedeadbeef"), new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, @@ -2156,38 +2645,40 @@ public void given__cloud_pullMerge_rev_userkey__when__build__then__scmBuilt() th assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); - assertThat(instance.remote(), is("git@bitbucket.org:qa/qa-repo.git")); + ), + List.of() + ); + assertThat(instance.remote(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); - assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); assertThat(actual.getGitTool(), nullValue()); assertThat(actual.getUserRemoteConfigs(), hasSize(2)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); assertThat(config.getRefspec(), is("+refs/heads/qa-branch:refs/remotes/origin/PR-1")); - assertThat(config.getUrl(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(config.getUrl(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(config.getCredentialsId(), is("user-key")); config = actual.getUserRemoteConfigs().get(1); assertThat(config.getName(), is("upstream")); assertThat(config.getRefspec(), is("+refs/heads/test-branch:refs/remotes/upstream/test-branch")); - assertThat(config.getUrl(), is("git@bitbucket.org:tester/test-repo.git")); + assertThat(config.getUrl(), is("ssh://git@bitbucket.org/tester/test-repo.git")); assertThat(config.getCredentialsId(), is("user-key")); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(origin.getURIs().get(0).toString(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(origin.getFetchRefSpecs(), hasSize(1)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/qa-branch")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); @@ -2196,7 +2687,7 @@ public void given__cloud_pullMerge_rev_userkey__when__build__then__scmBuilt() th origin = actual.getRepositoryByName("upstream"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("git@bitbucket.org:tester/test-repo.git")); + assertThat(origin.getURIs().get(0).toString(), is("ssh://git@bitbucket.org/tester/test-repo.git")); assertThat(origin.getFetchRefSpecs(), hasSize(1)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/test-branch")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/upstream/test-branch")); @@ -2234,22 +2725,24 @@ public void given__cloud_pullMerge_norev_anon__when__build__then__scmBuilt() thr assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is(nullValue())); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.org/qa/qa-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); - assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); assertThat(actual.getGitTool(), nullValue()); assertThat(actual.getUserRemoteConfigs(), hasSize(2)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); @@ -2300,22 +2793,24 @@ public void given__cloud_pullMerge_norev_userpass__when__build__then__scmBuilt() assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is(nullValue())); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.org/qa/qa-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); - assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); assertThat(actual.getGitTool(), nullValue()); assertThat(actual.getUserRemoteConfigs(), hasSize(2)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); @@ -2366,38 +2861,40 @@ public void given__cloud_pullMerge_norev_userkey__when__build__then__scmBuilt() assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is(nullValue())); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.org")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.org/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") - )); - assertThat(instance.remote(), is("git@bitbucket.org:qa/qa-repo.git")); + ), + List.of() + ); + assertThat(instance.remote(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); + assertThat(instance.refSpecs(), contains("+refs/heads/qa-branch:refs/remotes/@{remote}/PR-1")); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); - assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/tester/test-repo")); assertThat(actual.getGitTool(), nullValue()); assertThat(actual.getUserRemoteConfigs(), hasSize(2)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); assertThat(config.getRefspec(), is("+refs/heads/qa-branch:refs/remotes/origin/PR-1")); - assertThat(config.getUrl(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(config.getUrl(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(config.getCredentialsId(), is("user-key")); config = actual.getUserRemoteConfigs().get(1); assertThat(config.getName(), is("upstream")); assertThat(config.getRefspec(), is("+refs/heads/test-branch:refs/remotes/upstream/test-branch")); - assertThat(config.getUrl(), is("git@bitbucket.org:tester/test-repo.git")); + assertThat(config.getUrl(), is("ssh://git@bitbucket.org/tester/test-repo.git")); assertThat(config.getCredentialsId(), is("user-key")); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("git@bitbucket.org:qa/qa-repo.git")); + assertThat(origin.getURIs().get(0).toString(), is("ssh://git@bitbucket.org/qa/qa-repo.git")); assertThat(origin.getFetchRefSpecs(), hasSize(1)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/qa-branch")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); @@ -2406,7 +2903,7 @@ public void given__cloud_pullMerge_norev_userkey__when__build__then__scmBuilt() origin = actual.getRepositoryByName("upstream"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("git@bitbucket.org:tester/test-repo.git")); + assertThat(origin.getURIs().get(0).toString(), is("ssh://git@bitbucket.org/tester/test-repo.git")); assertThat(origin.getFetchRefSpecs(), hasSize(1)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/test-branch")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/upstream/test-branch")); @@ -2427,8 +2924,8 @@ public void given__server_pullMerge_rev_anon__when__build__then__scmBuilt() thro PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), ChangeRequestCheckoutStrategy.MERGE); - PullRequestSCMRevision revision = - new PullRequestSCMRevision<>(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), "deadbeefcafebabedeadbeefcafebabedeadbeef"), new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, @@ -2437,52 +2934,50 @@ public void given__server_pullMerge_rev_anon__when__build__then__scmBuilt() thro assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.test")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + assertThat( + instance.refSpecs(), + contains( + "+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1", + "+refs/heads/test-branch:refs/remotes/@{remote}/test-branch" + ) + ); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); assertThat(actual.getGitTool(), nullValue()); - assertThat(actual.getUserRemoteConfigs(), hasSize(2)); + assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); - assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1")); - assertThat(config.getUrl(), is("https://bitbucket.test/scm/tester/test-repo.git")); - assertThat(config.getCredentialsId(), is(nullValue())); - config = actual.getUserRemoteConfigs().get(1); - assertThat(config.getName(), is("upstream")); - assertThat(config.getRefspec(), is("+refs/heads/test-branch:refs/remotes/upstream/test-branch")); + assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1 +refs/heads/test-branch:refs/remotes/origin/test-branch")); assertThat(config.getUrl(), is("https://bitbucket.test/scm/tester/test-repo.git")); - assertThat(config.getCredentialsId(), is(nullValue())); + assertThat(config.getCredentialsId(), nullValue()); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); assertThat(origin.getURIs().get(0).toString(), is("https://bitbucket.test/scm/tester/test-repo.git")); - assertThat(origin.getFetchRefSpecs(), hasSize(1)); + assertThat(origin.getFetchRefSpecs(), hasSize(2)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/pull-requests/1/from")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); - origin = actual.getRepositoryByName("upstream"); - assertThat(origin, notNullValue()); - assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("https://bitbucket.test/scm/tester/test-repo.git")); - assertThat(origin.getFetchRefSpecs(), hasSize(1)); - assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/test-branch")); - assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/upstream/test-branch")); - assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); - assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(origin.getFetchRefSpecs().get(1).getSource(), is("refs/heads/test-branch")); + assertThat(origin.getFetchRefSpecs().get(1).getDestination(), is("refs/remotes/origin/test-branch")); + assertThat(origin.getFetchRefSpecs().get(1).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(1).isWildcard(), is(false)); assertThat(actual.getExtensions(), containsInAnyOrder( instanceOf(MergeWithGitSCMExtension.class), instanceOf(BuildChooserSetting.class), @@ -2500,7 +2995,7 @@ public void given__server_pullMerge_rev_anon__when__build__then__scmBuilt() thro assertThat(revisions.iterator().next().getSha1String(), is("cafebabedeadbeefcafebabedeadbeefcafebabe")); MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); assertThat(merge, notNullValue()); - assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); + assertThat(merge.getBaseName(), is("remotes/origin/test-branch")); assertThat(merge.getBaseHash(), is("deadbeefcafebabedeadbeefcafebabedeadbeef")); } @@ -2511,8 +3006,8 @@ public void given__server_pullMerge_rev_userpass__when__build__then__scmBuilt() PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), ChangeRequestCheckoutStrategy.MERGE); - PullRequestSCMRevision revision = - new PullRequestSCMRevision<>(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), "deadbeefcafebabedeadbeefcafebabedeadbeef"), new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, @@ -2521,52 +3016,50 @@ public void given__server_pullMerge_rev_userpass__when__build__then__scmBuilt() assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.test")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + assertThat( + instance.refSpecs(), + contains( + "+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1", + "+refs/heads/test-branch:refs/remotes/@{remote}/test-branch" + ) + ); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); assertThat(actual.getGitTool(), nullValue()); - assertThat(actual.getUserRemoteConfigs(), hasSize(2)); + assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); - assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1")); - assertThat(config.getUrl(), is("https://bitbucket.test/scm/tester/test-repo.git")); - assertThat(config.getCredentialsId(), is("user-pass")); - config = actual.getUserRemoteConfigs().get(1); - assertThat(config.getName(), is("upstream")); - assertThat(config.getRefspec(), is("+refs/heads/test-branch:refs/remotes/upstream/test-branch")); + assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1 +refs/heads/test-branch:refs/remotes/origin/test-branch")); assertThat(config.getUrl(), is("https://bitbucket.test/scm/tester/test-repo.git")); assertThat(config.getCredentialsId(), is("user-pass")); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); assertThat(origin.getURIs().get(0).toString(), is("https://bitbucket.test/scm/tester/test-repo.git")); - assertThat(origin.getFetchRefSpecs(), hasSize(1)); + assertThat(origin.getFetchRefSpecs(), hasSize(2)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/pull-requests/1/from")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); - origin = actual.getRepositoryByName("upstream"); - assertThat(origin, notNullValue()); - assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("https://bitbucket.test/scm/tester/test-repo.git")); - assertThat(origin.getFetchRefSpecs(), hasSize(1)); - assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/test-branch")); - assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/upstream/test-branch")); - assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); - assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(origin.getFetchRefSpecs().get(1).getSource(), is("refs/heads/test-branch")); + assertThat(origin.getFetchRefSpecs().get(1).getDestination(), is("refs/remotes/origin/test-branch")); + assertThat(origin.getFetchRefSpecs().get(1).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(1).isWildcard(), is(false)); assertThat(actual.getExtensions(), containsInAnyOrder( instanceOf(MergeWithGitSCMExtension.class), instanceOf(BuildChooserSetting.class), @@ -2584,7 +3077,7 @@ public void given__server_pullMerge_rev_userpass__when__build__then__scmBuilt() assertThat(revisions.iterator().next().getSha1String(), is("cafebabedeadbeefcafebabedeadbeefcafebabe")); MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); assertThat(merge, notNullValue()); - assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); + assertThat(merge.getBaseName(), is("remotes/origin/test-branch")); assertThat(merge.getBaseHash(), is("deadbeefcafebabedeadbeefcafebabedeadbeef")); } @@ -2594,8 +3087,8 @@ public void given__server_pullMerge_rev_userkey__when__build__then__scmBuilt() t PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), ChangeRequestCheckoutStrategy.MERGE); - PullRequestSCMRevision revision = - new PullRequestSCMRevision<>(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), "deadbeefcafebabedeadbeefcafebabedeadbeef"), new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, @@ -2604,52 +3097,50 @@ public void given__server_pullMerge_rev_userkey__when__build__then__scmBuilt() t assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.test")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("ssh://git@bitbucket.test:7999/tester/test-repo.git")); + assertThat( + instance.refSpecs(), + contains( + "+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1", + "+refs/heads/test-branch:refs/remotes/@{remote}/test-branch" + ) + ); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); assertThat(actual.getGitTool(), nullValue()); - assertThat(actual.getUserRemoteConfigs(), hasSize(2)); + assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); - assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1")); - assertThat(config.getUrl(), is("ssh://git@bitbucket.test:7999/tester/test-repo.git")); - assertThat(config.getCredentialsId(), is("user-key")); - config = actual.getUserRemoteConfigs().get(1); - assertThat(config.getName(), is("upstream")); - assertThat(config.getRefspec(), is("+refs/heads/test-branch:refs/remotes/upstream/test-branch")); + assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1 +refs/heads/test-branch:refs/remotes/origin/test-branch")); assertThat(config.getUrl(), is("ssh://git@bitbucket.test:7999/tester/test-repo.git")); assertThat(config.getCredentialsId(), is("user-key")); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); assertThat(origin.getURIs().get(0).toString(), is("ssh://git@bitbucket.test:7999/tester/test-repo.git")); - assertThat(origin.getFetchRefSpecs(), hasSize(1)); + assertThat(origin.getFetchRefSpecs(), hasSize(2)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/pull-requests/1/from")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); - origin = actual.getRepositoryByName("upstream"); - assertThat(origin, notNullValue()); - assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("ssh://git@bitbucket.test:7999/tester/test-repo.git")); - assertThat(origin.getFetchRefSpecs(), hasSize(1)); - assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/test-branch")); - assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/upstream/test-branch")); - assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); - assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(origin.getFetchRefSpecs().get(1).getSource(), is("refs/heads/test-branch")); + assertThat(origin.getFetchRefSpecs().get(1).getDestination(), is("refs/remotes/origin/test-branch")); + assertThat(origin.getFetchRefSpecs().get(1).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(1).isWildcard(), is(false)); assertThat(actual.getExtensions(), containsInAnyOrder( instanceOf(MergeWithGitSCMExtension.class), instanceOf(BuildChooserSetting.class), @@ -2667,7 +3158,7 @@ public void given__server_pullMerge_rev_userkey__when__build__then__scmBuilt() t assertThat(revisions.iterator().next().getSha1String(), is("cafebabedeadbeefcafebabedeadbeefcafebabe")); MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); assertThat(merge, notNullValue()); - assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); + assertThat(merge.getBaseName(), is("remotes/origin/test-branch")); assertThat(merge.getBaseHash(), is("deadbeefcafebabedeadbeefcafebabedeadbeef")); } @@ -2677,8 +3168,8 @@ public void given__server_pullMerge_rev_userkey__when_different_clone_url__build PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", "a fake title", new BranchSCMHead("test-branch"), new SCMHeadOrigin.Fork("qa/qa-repo"), ChangeRequestCheckoutStrategy.MERGE); - PullRequestSCMRevision revision = - new PullRequestSCMRevision<>(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), + PullRequestSCMRevision revision = + new PullRequestSCMRevision(head, new AbstractGitSCMSource.SCMRevisionImpl(head.getTarget(), "deadbeefcafebabedeadbeefcafebabedeadbeef"), new AbstractGitSCMSource.SCMRevisionImpl(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, @@ -2687,54 +3178,52 @@ public void given__server_pullMerge_rev_userkey__when_different_clone_url__build assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is((SCMRevision) revision)); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://www.bitbucket.test/web")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://www.bitbucket.test/web/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@www.bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://www.bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git")); + assertThat( + instance.refSpecs(), + contains( + "+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1", + "+refs/heads/test-branch:refs/remotes/@{remote}/test-branch" + ) + ); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); assertThat(actual.getBrowser().getRepoUrl(), is("https://www.bitbucket.test/web/projects/tester/repos/test-repo")); assertThat(actual.getGitTool(), nullValue()); - assertThat(actual.getUserRemoteConfigs(), hasSize(2)); + assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); - assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1")); - assertThat(config.getUrl(), is("ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git")); - assertThat(config.getCredentialsId(), is("user-key")); - config = actual.getUserRemoteConfigs().get(1); - assertThat(config.getName(), is("upstream")); - assertThat(config.getRefspec(), is("+refs/heads/test-branch:refs/remotes/upstream/test-branch")); + assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1 +refs/heads/test-branch:refs/remotes/origin/test-branch")); assertThat(config.getUrl(), is("ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git")); assertThat(config.getCredentialsId(), is("user-key")); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); assertThat(origin.getURIs().get(0).toString(), is("ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git")); - assertThat(origin.getFetchRefSpecs(), hasSize(1)); + assertThat(origin.getFetchRefSpecs(), hasSize(2)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/pull-requests/1/from")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); - origin = actual.getRepositoryByName("upstream"); - assertThat(origin, notNullValue()); - assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git")); - assertThat(origin.getFetchRefSpecs(), hasSize(1)); - assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/test-branch")); - assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/upstream/test-branch")); - assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); - assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(origin.getFetchRefSpecs().get(1).getSource(), is("refs/heads/test-branch")); + assertThat(origin.getFetchRefSpecs().get(1).getDestination(), is("refs/remotes/origin/test-branch")); + assertThat(origin.getFetchRefSpecs().get(1).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(1).isWildcard(), is(false)); assertThat(actual.getExtensions(), containsInAnyOrder( instanceOf(MergeWithGitSCMExtension.class), instanceOf(BuildChooserSetting.class), @@ -2752,7 +3241,7 @@ public void given__server_pullMerge_rev_userkey__when_different_clone_url__build assertThat(revisions.iterator().next().getSha1String(), is("cafebabedeadbeefcafebabedeadbeefcafebabe")); MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); assertThat(merge, notNullValue()); - assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); + assertThat(merge.getBaseName(), is("remotes/origin/test-branch")); assertThat(merge.getBaseHash(), is("deadbeefcafebabedeadbeefcafebabedeadbeef")); } @@ -2768,59 +3257,57 @@ public void given__server_pullMerge_norev_anon__when__build__then__scmBuilt() th assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is(nullValue())); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.test")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + assertThat( + instance.refSpecs(), + contains( + "+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1", + "+refs/heads/test-branch:refs/remotes/@{remote}/test-branch" + ) + ); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); assertThat(actual.getGitTool(), nullValue()); - assertThat(actual.getUserRemoteConfigs(), hasSize(2)); + assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); - assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1")); - assertThat(config.getUrl(), is("https://bitbucket.test/scm/tester/test-repo.git")); - assertThat(config.getCredentialsId(), is(nullValue())); - config = actual.getUserRemoteConfigs().get(1); - assertThat(config.getName(), is("upstream")); - assertThat(config.getRefspec(), is("+refs/heads/test-branch:refs/remotes/upstream/test-branch")); + assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1 +refs/heads/test-branch:refs/remotes/origin/test-branch")); assertThat(config.getUrl(), is("https://bitbucket.test/scm/tester/test-repo.git")); - assertThat(config.getCredentialsId(), is(nullValue())); + assertThat(config.getCredentialsId(), nullValue()); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); assertThat(origin.getURIs().get(0).toString(), is("https://bitbucket.test/scm/tester/test-repo.git")); - assertThat(origin.getFetchRefSpecs(), hasSize(1)); + assertThat(origin.getFetchRefSpecs(), hasSize(2)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/pull-requests/1/from")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); - origin = actual.getRepositoryByName("upstream"); - assertThat(origin, notNullValue()); - assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("https://bitbucket.test/scm/tester/test-repo.git")); - assertThat(origin.getFetchRefSpecs(), hasSize(1)); - assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/test-branch")); - assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/upstream/test-branch")); - assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); - assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(origin.getFetchRefSpecs().get(1).getSource(), is("refs/heads/test-branch")); + assertThat(origin.getFetchRefSpecs().get(1).getDestination(), is("refs/remotes/origin/test-branch")); + assertThat(origin.getFetchRefSpecs().get(1).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(1).isWildcard(), is(false)); assertThat(actual.getExtensions(), containsInAnyOrder( instanceOf(MergeWithGitSCMExtension.class), instanceOf(GitSCMSourceDefaults.class)) ); MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); assertThat(merge, notNullValue()); - assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); + assertThat(merge.getBaseName(), is("remotes/origin/test-branch")); assertThat(merge.getBaseHash(), is(nullValue())); } @@ -2836,59 +3323,57 @@ public void given__server_pullMerge_norev_userpass__when__build__then__scmBuilt( assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is(nullValue())); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.test")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + assertThat( + instance.refSpecs(), + contains( + "+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1", + "+refs/heads/test-branch:refs/remotes/@{remote}/test-branch" + ) + ); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); assertThat(actual.getGitTool(), nullValue()); - assertThat(actual.getUserRemoteConfigs(), hasSize(2)); + assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); - assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1")); - assertThat(config.getUrl(), is("https://bitbucket.test/scm/tester/test-repo.git")); - assertThat(config.getCredentialsId(), is("user-pass")); - config = actual.getUserRemoteConfigs().get(1); - assertThat(config.getName(), is("upstream")); - assertThat(config.getRefspec(), is("+refs/heads/test-branch:refs/remotes/upstream/test-branch")); + assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1 +refs/heads/test-branch:refs/remotes/origin/test-branch")); assertThat(config.getUrl(), is("https://bitbucket.test/scm/tester/test-repo.git")); assertThat(config.getCredentialsId(), is("user-pass")); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); assertThat(origin.getURIs().get(0).toString(), is("https://bitbucket.test/scm/tester/test-repo.git")); - assertThat(origin.getFetchRefSpecs(), hasSize(1)); + assertThat(origin.getFetchRefSpecs(), hasSize(2)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/pull-requests/1/from")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); - origin = actual.getRepositoryByName("upstream"); - assertThat(origin, notNullValue()); - assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("https://bitbucket.test/scm/tester/test-repo.git")); - assertThat(origin.getFetchRefSpecs(), hasSize(1)); - assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/test-branch")); - assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/upstream/test-branch")); - assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); - assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(origin.getFetchRefSpecs().get(1).getSource(), is("refs/heads/test-branch")); + assertThat(origin.getFetchRefSpecs().get(1).getDestination(), is("refs/remotes/origin/test-branch")); + assertThat(origin.getFetchRefSpecs().get(1).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(1).isWildcard(), is(false)); assertThat(actual.getExtensions(), containsInAnyOrder( instanceOf(MergeWithGitSCMExtension.class), instanceOf(GitSCMSourceDefaults.class)) ); MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); assertThat(merge, notNullValue()); - assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); + assertThat(merge.getBaseName(), is("remotes/origin/test-branch")); assertThat(merge.getBaseHash(), is(nullValue())); } @@ -2904,59 +3389,57 @@ public void given__server_pullMerge_norev_userkey__when__build__then__scmBuilt() assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is(nullValue())); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://bitbucket.test")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("ssh://git@bitbucket.test:7999/tester/test-repo.git")); + assertThat( + instance.refSpecs(), + contains( + "+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1", + "+refs/heads/test-branch:refs/remotes/@{remote}/test-branch" + ) + ); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.test/projects/tester/repos/test-repo")); assertThat(actual.getGitTool(), nullValue()); - assertThat(actual.getUserRemoteConfigs(), hasSize(2)); + assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); - assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1")); - assertThat(config.getUrl(), is("ssh://git@bitbucket.test:7999/tester/test-repo.git")); - assertThat(config.getCredentialsId(), is("user-key")); - config = actual.getUserRemoteConfigs().get(1); - assertThat(config.getName(), is("upstream")); - assertThat(config.getRefspec(), is("+refs/heads/test-branch:refs/remotes/upstream/test-branch")); + assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1 +refs/heads/test-branch:refs/remotes/origin/test-branch")); assertThat(config.getUrl(), is("ssh://git@bitbucket.test:7999/tester/test-repo.git")); assertThat(config.getCredentialsId(), is("user-key")); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); assertThat(origin.getURIs().get(0).toString(), is("ssh://git@bitbucket.test:7999/tester/test-repo.git")); - assertThat(origin.getFetchRefSpecs(), hasSize(1)); + assertThat(origin.getFetchRefSpecs(), hasSize(2)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/pull-requests/1/from")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); - origin = actual.getRepositoryByName("upstream"); - assertThat(origin, notNullValue()); - assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("ssh://git@bitbucket.test:7999/tester/test-repo.git")); - assertThat(origin.getFetchRefSpecs(), hasSize(1)); - assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/test-branch")); - assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/upstream/test-branch")); - assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); - assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(origin.getFetchRefSpecs().get(1).getSource(), is("refs/heads/test-branch")); + assertThat(origin.getFetchRefSpecs().get(1).getDestination(), is("refs/remotes/origin/test-branch")); + assertThat(origin.getFetchRefSpecs().get(1).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(1).isWildcard(), is(false)); assertThat(actual.getExtensions(), containsInAnyOrder( instanceOf(MergeWithGitSCMExtension.class), instanceOf(GitSCMSourceDefaults.class)) ); MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); assertThat(merge, notNullValue()); - assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); + assertThat(merge.getBaseName(), is("remotes/origin/test-branch")); assertThat(merge.getBaseHash(), is(nullValue())); } @@ -2972,61 +3455,59 @@ public void given__server_pullMerge_norev_userkey_different_clone_url__when__bui assertThat(instance.head(), is((SCMHead) head)); assertThat(instance.revision(), is(nullValue())); assertThat(instance.scmSource(), is(source)); - assertThat(instance.refSpecs(), contains("+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1")); - assertThat(instance.cloneLinks(), is(Collections.emptyList())); assertThat("expecting dummy value until clone links provided or withBitbucketRemote called", instance.remote(), is("https://www.bitbucket.test/web")); assertThat(instance.browser(), instanceOf(BitbucketWeb.class)); assertThat(instance.browser().getRepoUrl(), is("https://www.bitbucket.test/web/projects/tester/repos/test-repo")); - instance.withCloneLinks(Arrays.asList( - new BitbucketHref("https", "https://tester@www.bitbucket.test/scm/tester/test-repo.git"), + instance.withCloneLinks( + List.of( + new BitbucketHref("http", "https://www.bitbucket.test/scm/tester/test-repo.git"), new BitbucketHref("ssh", "ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git") - )); + ), + List.of() + ); assertThat(instance.remote(), is("ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git")); + assertThat( + instance.refSpecs(), + contains( + "+refs/pull-requests/1/from:refs/remotes/@{remote}/PR-1", + "+refs/heads/test-branch:refs/remotes/@{remote}/test-branch" + ) + ); GitSCM actual = instance.build(); assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); assertThat(actual.getBrowser().getRepoUrl(), is("https://www.bitbucket.test/web/projects/tester/repos/test-repo")); assertThat(actual.getGitTool(), nullValue()); - assertThat(actual.getUserRemoteConfigs(), hasSize(2)); + assertThat(actual.getUserRemoteConfigs(), hasSize(1)); UserRemoteConfig config = actual.getUserRemoteConfigs().get(0); assertThat(config.getName(), is("origin")); - assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1")); - assertThat(config.getUrl(), is("ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git")); - assertThat(config.getCredentialsId(), is("user-key")); - config = actual.getUserRemoteConfigs().get(1); - assertThat(config.getName(), is("upstream")); - assertThat(config.getRefspec(), is("+refs/heads/test-branch:refs/remotes/upstream/test-branch")); + assertThat(config.getRefspec(), is("+refs/pull-requests/1/from:refs/remotes/origin/PR-1 +refs/heads/test-branch:refs/remotes/origin/test-branch")); assertThat(config.getUrl(), is("ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git")); assertThat(config.getCredentialsId(), is("user-key")); RemoteConfig origin = actual.getRepositoryByName("origin"); assertThat(origin, notNullValue()); assertThat(origin.getURIs(), hasSize(1)); assertThat(origin.getURIs().get(0).toString(), is("ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git")); - assertThat(origin.getFetchRefSpecs(), hasSize(1)); + assertThat(origin.getFetchRefSpecs(), hasSize(2)); assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/pull-requests/1/from")); assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/origin/PR-1")); assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); - origin = actual.getRepositoryByName("upstream"); - assertThat(origin, notNullValue()); - assertThat(origin.getURIs(), hasSize(1)); - assertThat(origin.getURIs().get(0).toString(), is("ssh://git@ssh.bitbucket.test:7999/tester/test-repo.git")); - assertThat(origin.getFetchRefSpecs(), hasSize(1)); - assertThat(origin.getFetchRefSpecs().get(0).getSource(), is("refs/heads/test-branch")); - assertThat(origin.getFetchRefSpecs().get(0).getDestination(), is("refs/remotes/upstream/test-branch")); - assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); - assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(origin.getFetchRefSpecs().get(1).getSource(), is("refs/heads/test-branch")); + assertThat(origin.getFetchRefSpecs().get(1).getDestination(), is("refs/remotes/origin/test-branch")); + assertThat(origin.getFetchRefSpecs().get(1).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(1).isWildcard(), is(false)); assertThat(actual.getExtensions(), containsInAnyOrder( instanceOf(MergeWithGitSCMExtension.class), instanceOf(GitSCMSourceDefaults.class)) ); MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); assertThat(merge, notNullValue()); - assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); + assertThat(merge.getBaseName(), is("remotes/origin/test-branch")); assertThat(merge.getBaseHash(), is(nullValue())); } diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMRevisionTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMRevisionTest.java index d855f3c5d..28f0672c2 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMRevisionTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMRevisionTest.java @@ -99,8 +99,8 @@ public void verify_revision_informations_are_valued() throws Exception { assertRevision(revision); } else if (head instanceof PullRequestSCMHead) { @SuppressWarnings("unchecked") - PullRequestSCMRevision revision = (PullRequestSCMRevision) 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); diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchScanningTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchScanningTest.java index 07825c055..749c32bf4 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchScanningTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchScanningTest.java @@ -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 diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/UriResolverTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/UriResolverTest.java deleted file mode 100644 index e9db9d561..000000000 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/UriResolverTest.java +++ /dev/null @@ -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" - )); - } - -} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfigurationTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfigurationTest.java index 2683c7340..3f606543f 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfigurationTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfigurationTest.java @@ -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)); } diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfigurationTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfigurationTest.java index 2d48fe018..5ab3fd3f8 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfigurationTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfigurationTest.java @@ -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(); diff --git a/src/test/java/integration/ScanningFailuresTest.java b/src/test/java/integration/ScanningFailuresTest.java index e0817960c..fe90d9412 100644 --- a/src/test/java/integration/ScanningFailuresTest.java +++ b/src/test/java/integration/ScanningFailuresTest.java @@ -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> 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 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"); diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfigurationTest/configuration-as-code.yml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfigurationTest/configuration-as-code.yml index 1c7a91a62..eb40274cb 100644 --- a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfigurationTest/configuration-as-code.yml +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfigurationTest/configuration-as-code.yml @@ -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 diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfigurationTest/configuration-as-code.yml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfigurationTest/configuration-as-code.yml index 602dcfe20..4d9395bf2 100644 --- a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfigurationTest/configuration-as-code.yml +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookConfigurationTest/configuration-as-code.yml @@ -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"