From a4c6bf39b83168ff62fc622bd4084ef90cf810c0 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Mon, 12 Jun 2017 16:04:15 +0100 Subject: [PATCH] [JENKINS-43507] Switch to trait based configuration Also: - Adds support for building the merge commits of pull requests - Adds support for trusted revisions - Adds support for configuring Git extensions and Mercurial additional behaviours - Adds lots of tests (traits make code that was previously hard to test easier to test) Should: - Migrate existing configuration to the same effective configuration - Maintain backwards binary compatibility - Not maintain backwards source compatibility (if you wrote a plugin to the old APIs you will need to fix when you upgrade the plugin dependency) --- pom.xml | 36 +- .../BitbucketBuildStatusNotifications.java | 184 +- .../bitbucket/BitbucketCredentials.java | 57 +- .../bitbucket/BitbucketGitSCMBuilder.java | 272 ++ .../bitbucket/BitbucketHgSCMBuilder.java | 256 ++ .../bitbucket/BitbucketSCMNavigator.java | 594 ++++- .../plugins/bitbucket/BitbucketSCMSource.java | 1301 ++++++---- .../bitbucket/BitbucketSCMSourceBuilder.java | 125 + .../bitbucket/BitbucketSCMSourceContext.java | 24 + .../bitbucket/BitbucketSCMSourceRequest.java | 16 +- .../bitbucket/BranchDiscoveryTrait.java | 265 ++ .../ForkPullRequestDiscoveryTrait.java | 342 +++ .../plugins/bitbucket/LazyIterable.java | 40 + .../bitbucket/MergeWithGitSCMExtension.java | 128 + .../OriginPullRequestDiscoveryTrait.java | 188 ++ .../PublicRepoPullRequestFilterTrait.java | 79 + .../plugins/bitbucket/PullRequestSCMHead.java | 123 +- .../bitbucket/PullRequestSCMRevision.java | 103 + .../bitbucket/SCMHeadWithOwnerAndRepo.java | 56 +- .../plugins/bitbucket/SSHCheckoutTrait.java | 181 ++ .../bitbucket/WebhookRegistration.java | 47 + .../bitbucket/WebhookRegistrationTrait.java | 125 + .../hooks/PullRequestHookProcessor.java | 122 +- .../bitbucket/hooks/PushHookProcessor.java | 14 +- .../hooks/WebhookAutoRegisterListener.java | 109 +- .../BitbucketSCMNavigator/config.jelly | 45 +- .../help-autoRegisterHooks.jelly | 19 - .../help-bitbucketServerUrl.html | 5 - .../help-checkoutCredentialsId.html | 3 - .../help-credentialsId.html | 4 +- .../BitbucketSCMNavigator/help-pattern.html | 3 - .../BitbucketSCMNavigator/help-repoOwner.html | 10 +- .../BitbucketSCMNavigator/help-serverUrl.html | 5 + .../BitbucketSCMNavigator/help-traits.html | 27 + .../BitbucketSCMSource/config-detail.jelly | 40 +- .../help-autoRegisterHook.jelly | 19 - .../help-bitbucketServerUrl.html | 7 - .../help-checkoutCredentialsId.jelly | 8 - .../help-credentialsId.html | 3 + .../help-credentialsId.jelly | 9 - .../BitbucketSCMSource/help-repoOwner.html | 11 + .../BitbucketSCMSource/help-repoOwner.jelly | 8 - .../BitbucketSCMSource/help-repository.html | 3 + .../BitbucketSCMSource/help-serverUrl.html | 5 + .../BitbucketSCMSource/help-traits.html | 23 + .../bitbucket/BitbucketSCMSource/help.html | 10 +- .../BranchDiscoveryTrait/config.jelly | 8 + .../BranchDiscoveryTrait/help-strategyId.html | 20 + .../bitbucket/BranchDiscoveryTrait/help.html | 3 + .../config.jelly | 10 + .../help-strategyId.html | 15 + .../help-trust.html | 33 + .../ForkPullRequestDiscoveryTrait/help.html | 3 + .../plugins/bitbucket/Messages.properties | 26 +- .../config.jelly | 8 + .../help-strategyId.html | 15 + .../OriginPullRequestDiscoveryTrait/help.html | 3 + .../help.html | 5 + .../bitbucket/SSHCheckoutTrait/config.jelly | 7 + .../SSHCheckoutTrait/help-credentialsId.html | 3 + .../bitbucket/SSHCheckoutTrait/help.html | 9 + .../WebhookRegistrationTrait/config.jelly | 6 + .../WebhookRegistrationTrait/help-mode.html | 11 + .../WebhookRegistrationTrait/help.html | 24 + .../bitbucket/BitbucketClientMockUtils.java | 38 +- .../bitbucket/BitbucketGitSCMBuilderTest.java | 2210 +++++++++++++++++ .../bitbucket/BitbucketHgSCMBuilderTest.java | 456 ++++ .../bitbucket/BitbucketSCMNavigatorTest.java | 963 +++++++ .../bitbucket/BitbucketSCMSourceTest.java | 888 +++++++ .../bitbucket/BranchDiscoveryTraitTest.java | 99 + .../BranchScanningIntegrationTest.java | 39 +- .../plugins/bitbucket/BranchScanningTest.java | 65 +- .../ForkPullRequestDiscoveryTraitTest.java | 125 + .../OriginPullRequestDiscoveryTraitTest.java | 120 + .../PublicRepoPullRequestFilterTraitTest.java | 24 + .../SCMNavigatorIntegrationTest.java | 22 +- .../bitbucket/SSHCheckoutTraitTest.java | 137 + .../WebhookRegistrationTraitTest.java | 47 + .../bitbucket/WebhooksAutoregisterTest.java | 45 +- .../integration/ScanningFailuresTest.java | 19 +- .../BitbucketSCMNavigatorTest/basic_cloud.xml | 8 + .../basic_server.xml | 11 + .../exclude_branches.xml | 10 + .../limit_branches.xml | 10 + .../limit_repositories.xml | 11 + .../BitbucketSCMNavigatorTest/modern.xml | 6 + .../register_hooks.xml | 11 + .../use_agent_checkout.xml | 11 + .../basic_cloud_git.xml | 14 + .../BitbucketSCMSourceTest/basic_cloud_hg.xml | 13 + .../BitbucketSCMSourceTest/basic_server.xml | 14 + .../custom_checkout_credentials.xml | 14 + .../exclude_branches.xml | 14 + .../BitbucketSCMSourceTest/limit_branches.xml | 14 + .../BitbucketSCMSourceTest/modern.xml | 8 + .../BitbucketSCMSourceTest/register_hooks.xml | 14 + .../use_agent_checkout.xml | 14 + 97 files changed, 9680 insertions(+), 1077 deletions(-) create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilder.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketHgSCMBuilder.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceBuilder.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/LazyIterable.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/MergeWithGitSCMExtension.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTrait.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PullRequestSCMRevision.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistration.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait.java delete mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-autoRegisterHooks.jelly delete mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-bitbucketServerUrl.html delete mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-checkoutCredentialsId.html delete mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-pattern.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-serverUrl.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-traits.html delete mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-autoRegisterHook.jelly delete mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-bitbucketServerUrl.html delete mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-checkoutCredentialsId.jelly create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-credentialsId.html delete mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-credentialsId.jelly create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-repoOwner.html delete mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-repoOwner.jelly create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-repository.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-serverUrl.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-traits.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait/config.jelly create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait/help-strategyId.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait/help.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait/config.jelly create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait/help-strategyId.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait/help-trust.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait/help.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait/config.jelly create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait/help-strategyId.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait/help.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTrait/help.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait/config.jelly create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait/help-credentialsId.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait/help.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait/config.jelly create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait/help-mode.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait/help.html create mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilderTest.java create mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketHgSCMBuilderTest.java create mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest.java create mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest.java create mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTraitTest.java create mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTraitTest.java create mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTraitTest.java create mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTraitTest.java create mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTraitTest.java create mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTraitTest.java create mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/basic_cloud.xml create mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/basic_server.xml create mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/exclude_branches.xml create mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/limit_branches.xml create mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/limit_repositories.xml create mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/modern.xml create mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/register_hooks.xml create mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/use_agent_checkout.xml create mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/basic_cloud_git.xml create mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/basic_cloud_hg.xml create mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/basic_server.xml create mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/custom_checkout_credentials.xml create mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/exclude_branches.xml create mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/limit_branches.xml create mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/modern.xml create mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/register_hooks.xml create mode 100644 src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/use_agent_checkout.xml diff --git a/pom.xml b/pom.xml index 5aabb9ab4..3629d2e3d 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ cloudbees-bitbucket-branch-source - 2.1.3-SNAPSHOT + 2.2.0-SNAPSHOT hpi Bitbucket Branch Source Plugin @@ -48,7 +48,8 @@ 1.642.3 - 2.2.0-20170411.160059-4 + 2.2.0-SNAPSHOT + 3.4.0-SNAPSHOT @@ -58,16 +59,24 @@ HEAD + - - org.jenkins-ci.plugins - scm-api - ${scm-api.version} + + org.jenkins-ci.plugins + scm-api + ${scm-api.version} + + + + + + org.jenkins-ci.plugins + scm-api org.jenkins-ci.plugins git - 2.6.5 + ${git.version} org.apache.httpcomponents @@ -78,7 +87,7 @@ org.jenkins-ci.plugins mercurial - 1.58 + 2.0-SNAPSHOT org.codehaus.jackson @@ -93,7 +102,7 @@ org.jenkins-ci.plugins branch-api - 2.0.6 + 2.0.10-SNAPSHOT test @@ -118,7 +127,7 @@ org.jenkins-ci.plugins git - 2.6.5 + ${git.version} tests test @@ -156,6 +165,13 @@ org.jenkins-ci.tools maven-hpi-plugin + + + + generate-taglib-interface + + + 2.0.0 diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketBuildStatusNotifications.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketBuildStatusNotifications.java index 91470702c..d062c034b 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketBuildStatusNotifications.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketBuildStatusNotifications.java @@ -25,147 +25,108 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus; -import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; - -import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.FilePath; -import hudson.model.Item; -import hudson.model.ItemGroup; -import hudson.model.Job; import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; import hudson.model.listeners.RunListener; import hudson.model.listeners.SCMListener; -import hudson.plugins.git.Revision; -import hudson.plugins.git.util.BuildData; -import hudson.plugins.mercurial.MercurialTagAction; +import hudson.plugins.mercurial.MercurialSCMSource; import hudson.scm.SCM; import hudson.scm.SCMRevisionState; import java.io.File; import java.io.IOException; -import jenkins.scm.api.SCMHead; +import javax.annotation.CheckForNull; +import jenkins.plugins.git.AbstractGitSCMSource; +import jenkins.scm.api.SCMHeadObserver; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMRevisionAction; import jenkins.scm.api.SCMSource; -import jenkins.scm.api.SCMSourceOwner; import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider; /** * This class encapsulates all Bitbucket notifications logic. * {@link JobCompletedListener} sends a notification to Bitbucket after a build finishes. * Only builds derived from a job that was created as part of a multi branch project will be processed by this listener. - * - * The way the notification is sent is defined by the implementation of {@link BitbucketNotifier} returned by {@link #getNotifier(BitbucketApi)}. - * */ public class BitbucketBuildStatusNotifications { - private static void createBuildCommitStatus(@NonNull Run build, @NonNull TaskListener listener, @NonNull BitbucketApi bitbucket) + private static void createStatus(@NonNull Run build, @NonNull TaskListener listener, + @NonNull BitbucketApi bitbucket, @NonNull String hash) throws IOException, InterruptedException { - String revision = extractRevision(build); - if (revision != null) { - Result result = build.getResult(); - String url; - try { - url = DisplayURLProvider.get().getRunURL(build); - } catch (IllegalStateException e) { - listener.getLogger().println("Can not determine Jenkins root URL. Commit status notifications are disabled until a root URL is configured in Jenkins global configuration."); - return; - } - BitbucketBuildStatus status = null; - if (Result.SUCCESS.equals(result)) { - status = new BitbucketBuildStatus(revision, "This commit looks good", "SUCCESSFUL", url, build.getParent().getName(), build.getDisplayName()); - } else if (Result.UNSTABLE.equals(result)) { - status = new BitbucketBuildStatus(revision, "This commit has test failures", "FAILED", url, build.getParent().getName(), build.getDisplayName()); - } else if (Result.FAILURE.equals(result)) { - status = new BitbucketBuildStatus(revision, "There was a failure building this commit", "FAILED", url, build.getParent().getName(), build.getDisplayName()); - } else if (result != null) { // ABORTED etc. - status = new BitbucketBuildStatus(revision, "Something is wrong with the build of this commit", "FAILED", url, build.getParent().getName(), build.getDisplayName()); - } else { - status = new BitbucketBuildStatus(revision, "The tests have started...", "INPROGRESS", url, build.getParent().getName(), build.getDisplayName()); - } - if (status != null) { - getNotifier(bitbucket).buildStatus(status); - } - if (result != null) { - listener.getLogger().println("[Bitbucket] Build result notified"); - } + String url; + try { + url = DisplayURLProvider.get().getRunURL(build); + } catch (IllegalStateException e) { + listener.getLogger().println( + "Can not determine Jenkins root URL. Commit status notifications are disabled until a root URL is" + + " configured in Jenkins global configuration."); + return; } - } - - @CheckForNull - private static String extractRevision(Run build) { - String revision = null; - BuildData gitBuildData = build.getAction(BuildData.class); - if (gitBuildData != null) { - Revision lastBuiltRevision = gitBuildData.getLastBuiltRevision(); - if (lastBuiltRevision != null) { - revision = lastBuiltRevision.getSha1String(); - } + String key = build.getParent().getFullName(); // use the job full name as the key for the status + String name = build.getDisplayName(); // use the build number as the display name of the status + BitbucketBuildStatus status; + Result result = build.getResult(); + if (Result.SUCCESS.equals(result)) { + status = new BitbucketBuildStatus(hash, "This commit looks good", "SUCCESSFUL", url, key, name); + } else if (Result.UNSTABLE.equals(result)) { + status = new BitbucketBuildStatus(hash, "This commit has test failures", "FAILED", url, key, name); + } else if (Result.FAILURE.equals(result)) { + status = new BitbucketBuildStatus(hash, "There was a failure building this commit", "FAILED", url, key, + name); + } else if (result != null) { // ABORTED etc. + status = new BitbucketBuildStatus(hash, "Something is wrong with the build of this commit", "FAILED", url, + key, name); } else { - MercurialTagAction action = build.getAction(MercurialTagAction.class); - if (action != null) { - revision = action.getId(); - } + status = new BitbucketBuildStatus(hash, "The tests have started...", "INPROGRESS", url, key, name); + } + new BitbucketChangesetCommentNotifier(bitbucket).buildStatus(status); + if (result != null) { + listener.getLogger().println("[Bitbucket] Build result notified"); } - return revision; } - private static void createPullRequestCommitStatus(Run build, TaskListener listener, BitbucketApi bitbucket) + private static void sendNotifications(Run build, TaskListener listener) throws IOException, InterruptedException { - createBuildCommitStatus(build, listener, bitbucket); - } - - private static BitbucketNotifier getNotifier(BitbucketApi bitbucket) { - return new BitbucketChangesetCommentNotifier(bitbucket); - } + final SCMSource s = SCMSource.SourceByItem.findSource(build.getParent()); + if (!(s instanceof BitbucketSCMSource)) { + return; + } + BitbucketSCMSource source = (BitbucketSCMSource) s; + if (new BitbucketSCMSourceContext(null, SCMHeadObserver.none()) + .withTraits(source.getTraits()) + .notificationsDisabled()) { + return; + } + SCMRevision r = SCMRevisionAction.getRevision(build); // TODO JENKINS-44648 getRevision(s, build) + String hash = getHash(r); + if (hash == null) { + return; + } + if (r instanceof PullRequestSCMRevision) { + listener.getLogger().println("[Bitbucket] Notifying pull request build result"); + createStatus(build, listener, source.buildBitbucketClient((PullRequestSCMHead) r.getHead()), hash); - @CheckForNull - private static BitbucketSCMSource lookUpSCMSource(Run build) { - ItemGroup multiBranchProject = build.getParent().getParent(); - if (multiBranchProject instanceof SCMSourceOwner) { - SCMSourceOwner scmSourceOwner = (SCMSourceOwner) multiBranchProject; - BitbucketSCMSource source = lookUpBitbucketSCMSource(scmSourceOwner); - if (source != null) { - return source; - } + } else { + listener.getLogger().println("[Bitbucket] Notifying commit build result"); + createStatus(build, listener, source.buildBitbucketClient(), hash); } - return null; } - - - /** - * It is possible having more than one SCMSource in our MultiBranch project. - * TODO: Does it make sense having more than one of the same type? - * - * @param scmSourceOwner An {@link Item} that owns {@link SCMSource} instances. - * @return A source or null - */ @CheckForNull - private static BitbucketSCMSource lookUpBitbucketSCMSource(final SCMSourceOwner scmSourceOwner) { - for (SCMSource scmSource : scmSourceOwner.getSCMSources()) { - if (scmSource instanceof BitbucketSCMSource) { - return (BitbucketSCMSource) scmSource; - } + private static String getHash(@CheckForNull SCMRevision revision) { + if (revision instanceof PullRequestSCMRevision) { + // unwrap + revision = ((PullRequestSCMRevision) revision).getPull(); } - return null; - } - - private static void sendNotifications(Run build, TaskListener listener) - throws IOException, InterruptedException { - BitbucketSCMSource source = lookUpSCMSource(build); - if (source != null && extractRevision(build) != null) { - SCMHead head = SCMHead.HeadByItem.findHead(build.getParent()); - if (head instanceof PullRequestSCMHead) { - listener.getLogger().println("[Bitbucket] Notifying pull request build result"); - createPullRequestCommitStatus(build, listener, source.buildBitbucketClient((PullRequestSCMHead) head)); - } else { - listener.getLogger().println("[Bitbucket] Notifying commit build result"); - createBuildCommitStatus(build, listener, source.buildBitbucketClient()); - } + if (revision instanceof MercurialSCMSource.MercurialRevision) { + return ((MercurialSCMSource.MercurialRevision) revision).getHash(); + } else if (revision instanceof AbstractGitSCMSource.SCMRevisionImpl) { + return ((AbstractGitSCMSource.SCMRevisionImpl) revision).getHash(); } + return null; } /** @@ -175,8 +136,13 @@ private static void sendNotifications(Run build, TaskListener listener) public static class JobCheckOutListener extends SCMListener { @Override - public void onCheckout(Run build, SCM scm, FilePath workspace, TaskListener listener, File changelogFile, SCMRevisionState pollingBaseline) throws Exception { - sendNotifications(build, listener); + public void onCheckout(Run build, SCM scm, FilePath workspace, TaskListener listener, File changelogFile, + SCMRevisionState pollingBaseline) throws Exception { + try { + sendNotifications(build, listener); + } catch (IOException | InterruptedException e) { + e.printStackTrace(listener.error("Could not send notifications")); + } } } @@ -184,9 +150,9 @@ public void onCheckout(Run build, SCM scm, FilePath workspace, TaskListene * Sends notifications to Bitbucket on Run completed. */ @Extension - public static class JobCompletedListener extends RunListener> { + public static class JobCompletedListener extends RunListener> { - @Override + @Override public void onCompleted(Run build, TaskListener listener) { try { sendNotifications(build, listener); diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketCredentials.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketCredentials.java index 0962188e6..4041f3c0e 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketCredentials.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketCredentials.java @@ -23,21 +23,15 @@ */ package com.cloudbees.jenkins.plugins.bitbucket; -import com.cloudbees.plugins.credentials.CredentialsMatcher; import com.cloudbees.plugins.credentials.CredentialsMatchers; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardCredentials; -import com.cloudbees.plugins.credentials.common.StandardListBoxModel; -import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; -import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; -import com.cloudbees.plugins.credentials.domains.DomainRequirement; import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.model.Queue; import hudson.model.queue.Tasks; import hudson.security.ACL; -import java.util.List; import jenkins.scm.api.SCMSourceOwner; import org.apache.commons.lang.StringUtils; @@ -62,7 +56,7 @@ static T lookupCredentials(@CheckForNull String context instanceof Queue.Task ? Tasks.getDefaultAuthenticationOf((Queue.Task) context) : ACL.SYSTEM, - domainRequirementsOf(serverUrl) + URIRequirementBuilder.fromUri(serverUrl).build() ), CredentialsMatchers.allOf( CredentialsMatchers.withId(id), @@ -73,53 +67,4 @@ static T lookupCredentials(@CheckForNull String return null; } - static StandardListBoxModel fillCheckoutCredentials(@CheckForNull String serverUrl, - @NonNull SCMSourceOwner context, - @NonNull StandardListBoxModel result) { - result.includeMatchingAs( - context instanceof Queue.Task - ? Tasks.getDefaultAuthenticationOf((Queue.Task) context) - : ACL.SYSTEM, - context, - StandardCredentials.class, - domainRequirementsOf(serverUrl), - checkoutMatcher() - ); - return result; - } - - static StandardListBoxModel fillCredentials(@CheckForNull String serverUrl, - @NonNull SCMSourceOwner context, - @NonNull StandardListBoxModel result) { - result.includeMatchingAs( - context instanceof Queue.Task - ? Tasks.getDefaultAuthenticationOf((Queue.Task) context) - : ACL.SYSTEM, - context, - StandardUsernameCredentials.class, - domainRequirementsOf(serverUrl), - matcher() - ); - return result; - } - - /* package */ - static CredentialsMatcher matcher() { - return CredentialsMatchers.anyOf(CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class)); - } - - /* package */ - static CredentialsMatcher checkoutMatcher() { - return CredentialsMatchers.anyOf(CredentialsMatchers.instanceOf(StandardCredentials.class)); - } - - /* package */ - static List domainRequirementsOf(@CheckForNull String serverUrl) { - if (serverUrl == null) { - return URIRequirementBuilder.fromUri("https://bitbucket.org").build(); - } else { - return URIRequirementBuilder.fromUri(serverUrl).build(); - } - } - } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilder.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilder.java new file mode 100644 index 000000000..100fd3f48 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilder.java @@ -0,0 +1,272 @@ +/* + * The MIT License + * + * Copyright (c) 2017, 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.BitbucketHref; +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.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; +import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey; +import com.cloudbees.plugins.credentials.Credentials; +import com.cloudbees.plugins.credentials.common.IdCredentials; +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Util; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.browser.BitbucketWeb; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import jenkins.plugins.git.AbstractGitSCMSource; +import jenkins.plugins.git.GitSCMBuilder; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; +import org.apache.commons.lang.StringUtils; + +/** + * A {@link GitSCMBuilder} specialized for bitbucket. + * + * @since 2.2.0 + */ +public class BitbucketGitSCMBuilder extends GitSCMBuilder { + + /** + * The {@link BitbucketSCMSource} who's {@link BitbucketSCMSource#getOwner()} can be used as the context for + * resolving credentials. + */ + @NonNull + private final BitbucketSCMSource scmSource; + + /** + * The clone links for cloning the source repository and origin pull requests (but links will need tweaks for + * fork pull requests) + */ + @NonNull + private List cloneLinks = Collections.emptyList(); + + /** + * Constructor. + * + * @param scmSource the {@link BitbucketSCMSource}. + * @param head the {@link SCMHead} + * @param revision the (optional) {@link SCMRevision} + * @param credentialsId The {@link IdCredentials#getId()} of the {@link Credentials} to use when connecting to + * the {@link #remote()} or {@code null} to let the git client choose between providing its own + * credentials or connecting anonymously. + */ + public BitbucketGitSCMBuilder(@NonNull BitbucketSCMSource scmSource, @NonNull SCMHead head, + @CheckForNull SCMRevision revision, @CheckForNull String credentialsId) { + // 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 { + withRefSpec("+refs/heads/" + head.getName() + ":refs/remotes/@{remote}/" + head.getName()); + } + this.scmSource = scmSource; + AbstractBitbucketEndpoint endpoint = + BitbucketEndpointConfiguration.get().findEndpoint(scmSource.getServerUrl()); + if (endpoint == null) { + endpoint = new BitbucketServerEndpoint(null, scmSource.getServerUrl(), false, null); + } + withBrowser(new BitbucketWeb(endpoint.getRepositoryUrl( + scmSource.getRepoOwner(), + scmSource.getRepository() + ))); + } + + /** + * Provides the clone links from the {@link BitbucketRepository} to allow inference of ports for different protols. + * + * @param cloneLinks the clone links. + * @return {@code this} for method chaining. + */ + public BitbucketGitSCMBuilder withCloneLinks(List cloneLinks) { + this.cloneLinks = new ArrayList<>(Util.fixNull(cloneLinks)); + return withBitbucketRemote(); + } + + /** + * Returns the {@link BitbucketSCMSource} that this request is against (primarily to allow resolving credentials + * against {@link SCMSource#getOwner()}. + * + * @return the {@link BitbucketSCMSource} that this request is against + */ + @NonNull + 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); + } + + /** + * Updates the {@link GitSCMBuilder#withRemote(String)} based on the current {@link #head()} and + * {@link #revision()}. + * Will be called automatically by {@link #build()} but exposed in case the correct remote is required after + * changing the {@link #withCredentials(String)}. + * + * @return {@code this} for method chaining. + */ + @NonNull + public BitbucketGitSCMBuilder withBitbucketRemote() { + // Apply clone links and credentials + StandardCredentials credentials = StringUtils.isBlank(credentialsId()) + ? null + : BitbucketCredentials.lookupCredentials( + scmSource().getServerUrl(), + scmSource().getOwner(), + credentialsId(), + StandardCredentials.class + ); + Integer protocolPortOverride = null; + BitbucketRepositoryProtocol protocol = credentials instanceof SSHUserPrivateKey + ? BitbucketRepositoryProtocol.SSH + : BitbucketRepositoryProtocol.HTTP; + if (protocol == BitbucketRepositoryProtocol.SSH) { + for (BitbucketHref link : cloneLinks()) { + if ("ssh".equals(link.getName())) { + // extract the port from this link and use that + try { + URI uri = new URI(link.getHref()); + if (uri.getPort() != -1) { + protocolPortOverride = uri.getPort(); + } + } catch (URISyntaxException e) { + // ignore + } + break; + } + } + } + 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(); + } else { + // head instanceof BranchSCMHead + repoOwner = scmSource.getRepoOwner(); + repository = scmSource.getRepository(); + } + withRemote(bitbucket.getRepositoryUri( + BitbucketRepositoryType.GIT, + protocol, + protocolPortOverride, + repoOwner, + repository)); + AbstractBitbucketEndpoint endpoint = + BitbucketEndpointConfiguration.get().findEndpoint(scmSource.getServerUrl()); + if (endpoint == null) { + endpoint = new BitbucketServerEndpoint(null, scmSource.getServerUrl(), false, null); + } + 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; + + String remoteName = remoteName().equals("upstream") ? "upstream-upstream" : "upstream"; + withAdditionalRemote(remoteName, + bitbucket.getRepositoryUri( + BitbucketRepositoryType.GIT, + protocol, + protocolPortOverride, + scmSource().getRepoOwner(), + scmSource().getRepository()), + "+refs/heads/" + localName + ":refs/remotes/@{remote}/" + name); + 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)); + } + } + } + return this; + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public GitSCM build() { + withBitbucketRemote(); + SCMHead h = head(); + SCMRevision r = revision(); + try { + if (h instanceof PullRequestSCMHead) { + withHead(new SCMHead(((PullRequestSCMHead) h).getBranchName())); + if (r instanceof PullRequestSCMRevision) { + withRevision(((PullRequestSCMRevision) r).getPull()); + } + } + return super.build(); + } finally { + withHead(h); + withRevision(r); + } + } + +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketHgSCMBuilder.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketHgSCMBuilder.java new file mode 100644 index 000000000..4047fc20d --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketHgSCMBuilder.java @@ -0,0 +1,256 @@ +/* + * The MIT License + * + * Copyright (c) 2017, 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.BitbucketHref; +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.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; +import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey; +import com.cloudbees.plugins.credentials.Credentials; +import com.cloudbees.plugins.credentials.common.IdCredentials; +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Util; +import hudson.plugins.mercurial.MercurialSCM; +import hudson.plugins.mercurial.MercurialSCMBuilder; +import hudson.plugins.mercurial.MercurialSCMSource; +import hudson.plugins.mercurial.browser.BitBucket; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; +import org.apache.commons.lang.StringUtils; + +/** + * A {@link MercurialSCMBuilder} specialized for bitbucket. + * + * @since 2.2.0 + */ +public class BitbucketHgSCMBuilder extends MercurialSCMBuilder { + /** + * The {@link BitbucketSCMSource} who's {@link BitbucketSCMSource#getOwner()} can be used as the context for + * resolving credentials. + */ + @NonNull + private final BitbucketSCMSource scmSource; + + /** + * The clone links for cloning the source repository and origin pull requests (but links will need tweaks for + * fork pull requests) + */ + @NonNull + private List cloneLinks = Collections.emptyList(); + + /** + * Constructor. + * + * @param scmSource the {@link BitbucketSCMSource}. + * @param head the {@link SCMHead} + * @param revision the (optional) {@link SCMRevision} + * @param credentialsId The {@link IdCredentials#getId()} of the {@link Credentials} to use when connecting to + * the {@link #source()} or {@code null} to let the hg client choose between providing its own + * credentials or connecting anonymously. + */ + public BitbucketHgSCMBuilder(@NonNull BitbucketSCMSource scmSource, @NonNull SCMHead head, + @CheckForNull SCMRevision revision, String credentialsId) { + super(head, revision, /*dummy value*/scmSource.getServerUrl(), credentialsId); + this.scmSource = scmSource; + AbstractBitbucketEndpoint endpoint = + BitbucketEndpointConfiguration.get().findEndpoint(scmSource.getServerUrl()); + if (endpoint == null) { + endpoint = new BitbucketServerEndpoint(null, scmSource.getServerUrl(), false, null); + } + try { + withBrowser(new BitBucket(endpoint.getRepositoryUrl( + scmSource.getRepoOwner(), + scmSource.getRepository() + ))); + } catch (MalformedURLException e) { + // ignore, we are providing a well formed URL and if we are not then we shouldn't apply a browser + } + } + + /** + * Provides the clone links from the {@link BitbucketRepository} to allow inference of ports for different protols. + * + * @param cloneLinks the clone links. + * @return {@code this} for method chaining. + */ + public BitbucketHgSCMBuilder withCloneLinks(List cloneLinks) { + this.cloneLinks = new ArrayList<>(Util.fixNull(cloneLinks)); + return withBitbucketSource(); + } + + /** + * Returns the {@link BitbucketSCMSource} that this request is against (primarily to allow resolving credentials + * against {@link SCMSource#getOwner()}. + * + * @return the {@link BitbucketSCMSource} that this request is against + */ + @NonNull + 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); + } + + /** + * Updates the {@link MercurialSCMBuilder#withSource(String)} based on the current {@link #head()} and + * {@link #revision()}. + * Will be called automatically by {@link #build()} but exposed in case the correct remote is required after + * changing the {@link #withCredentialsId(String)}. + * + * @return {@code this} for method chaining. + */ + @NonNull + public BitbucketHgSCMBuilder withBitbucketSource() { + // Apply clone links and credentials + StandardCredentials credentials = StringUtils.isBlank(credentialsId()) + ? null + : BitbucketCredentials.lookupCredentials( + scmSource().getServerUrl(), + scmSource().getOwner(), + credentialsId(), + StandardCredentials.class + ); + Integer protocolPortOverride = null; + BitbucketRepositoryProtocol protocol = credentials instanceof SSHUserPrivateKey + ? BitbucketRepositoryProtocol.SSH + : BitbucketRepositoryProtocol.HTTP; + if (protocol == BitbucketRepositoryProtocol.SSH) { + for (BitbucketHref link : cloneLinks()) { + if ("ssh".equals(link.getName())) { + // extract the port from this link and use that + try { + URI uri = new URI(link.getHref()); + if (uri.getPort() != -1) { + protocolPortOverride = uri.getPort(); + } + } catch (URISyntaxException e) { + // ignore + } + break; + } + } + } + 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(); + } else { + // head instanceof BranchSCMHead + repoOwner = scmSource.getRepoOwner(); + repository = scmSource.getRepository(); + } + withSource(bitbucket.getRepositoryUri( + BitbucketRepositoryType.MERCURIAL, + protocol, + protocolPortOverride, + repoOwner, + repository)); + AbstractBitbucketEndpoint endpoint = + BitbucketEndpointConfiguration.get().findEndpoint(scmSource.getServerUrl()); + if (endpoint == null) { + endpoint = new BitbucketServerEndpoint(null, scmSource.getServerUrl(), false, null); + } + try { + withBrowser(new BitBucket(endpoint.getRepositoryUrl( + repoOwner, + repository + ))); + } catch (MalformedURLException e) { + // ignore, we are providing a well formed URL and if we are not then we shouldn't apply a browser + } + + // now, if we have to build a merge commit, let's ensure we build the merge commit! + if (h instanceof PullRequestSCMHead) { + PullRequestSCMHead head = (PullRequestSCMHead) h; + if (head.getCheckoutStrategy() == ChangeRequestCheckoutStrategy.MERGE) { + // TODO decorate with something that handles merge commits // FIXME file a Jenkins JIRA + } + } + return this; + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public MercurialSCM build() { + withBitbucketSource(); + SCMHead h = head(); + SCMRevision r = revision(); + try { + BitbucketSCMSource.MercurialRevision rev; + if (h instanceof PullRequestSCMHead) { + withHead(new SCMHead(((PullRequestSCMHead) h).getBranchName())); + if (r instanceof PullRequestSCMRevision) { + rev = ((PullRequestSCMRevision) r).getPull(); + } else if (r instanceof BitbucketSCMSource.MercurialRevision) { + rev = (BitbucketSCMSource.MercurialRevision) r; + } else { + rev = null; + } + } else { + rev = r instanceof BitbucketSCMSource.MercurialRevision + ? (BitbucketSCMSource.MercurialRevision) r : null; + } + if (rev != null) { + withRevision(new MercurialSCMSource.MercurialRevision(head(), rev.getHash())); + } + + return super.build(); + } finally { + withHead(h); + withRevision(r); + } + } + +} 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 54de31475..719cc20ec 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java @@ -29,37 +29,76 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketTeam; import com.cloudbees.jenkins.plugins.bitbucket.client.repository.UserRoleInRepository; +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.plugins.credentials.CredentialsMatchers; import com.cloudbees.plugins.credentials.CredentialsNameProvider; +import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.cloudbees.plugins.credentials.common.StandardListBoxModel; +import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; +import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; +import hudson.RestrictedSince; import hudson.Util; import hudson.console.HyperlinkNote; import hudson.model.Action; +import hudson.model.Queue; import hudson.model.TaskListener; +import hudson.model.queue.Tasks; +import hudson.plugins.git.GitSCM; +import hudson.plugins.mercurial.MercurialSCM; +import hudson.plugins.mercurial.traits.MercurialBrowserSCMSourceTrait; +import hudson.security.ACL; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import java.io.IOException; import java.io.ObjectStreamException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.regex.Pattern; -import javax.annotation.CheckForNull; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.plugins.git.traits.GitBrowserSCMSourceTrait; import jenkins.scm.api.SCMNavigator; import jenkins.scm.api.SCMNavigatorDescriptor; import jenkins.scm.api.SCMNavigatorEvent; import jenkins.scm.api.SCMNavigatorOwner; +import jenkins.scm.api.SCMSource; import jenkins.scm.api.SCMSourceCategory; import jenkins.scm.api.SCMSourceObserver; import jenkins.scm.api.SCMSourceOwner; import jenkins.scm.api.metadata.ObjectMetadataAction; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; +import jenkins.scm.api.trait.SCMNavigatorRequest; +import jenkins.scm.api.trait.SCMNavigatorTrait; +import jenkins.scm.api.trait.SCMNavigatorTraitDescriptor; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMTrait; +import jenkins.scm.api.trait.SCMTraitDescriptor; import jenkins.scm.impl.UncategorizedSCMSourceCategory; +import jenkins.scm.impl.form.NamedArrayList; +import jenkins.scm.impl.trait.Discovery; +import jenkins.scm.impl.trait.RegexSCMSourceFilterTrait; +import jenkins.scm.impl.trait.Selection; +import jenkins.scm.impl.trait.WildcardSCMHeadFilterTrait; import org.apache.commons.lang.StringUtils; import org.jenkins.ui.icon.Icon; import org.jenkins.ui.icon.IconSet; import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.DoNotUse; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; @@ -67,130 +106,338 @@ public class BitbucketSCMNavigator extends SCMNavigator { - private final String repoOwner; - private String credentialsId; - private String checkoutCredentialsId; - private String pattern = ".*"; - private boolean autoRegisterHooks = false; - private String bitbucketServerUrl; - /** - * Ant match expression that indicates what branches to include in the retrieve process. - */ - private String includes = "*"; + private static final Logger LOGGER = Logger.getLogger(BitbucketSCMSource.class.getName()); - /** - * Ant match expression that indicates what branches to exclude in the retrieve process. - */ - private String excludes = ""; + @NonNull + private String serverUrl; + @CheckForNull + private String credentialsId; + @NonNull + private final String repoOwner; + @NonNull + private List>> traits; + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + private transient String checkoutCredentialsId; + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + private transient String pattern; + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + private transient boolean autoRegisterHooks; + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + private transient String includes; + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + private transient String excludes; + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + private transient String bitbucketServerUrl; @DataBoundConstructor public BitbucketSCMNavigator(String repoOwner) { + this.serverUrl = BitbucketCloudEndpoint.SERVER_URL; this.repoOwner = repoOwner; + this.traits = new ArrayList<>(); this.credentialsId = null; // highlighting the default is anonymous unless you configure explicitly - this.checkoutCredentialsId = BitbucketSCMSource.DescriptorImpl.SAME; + this.traits.add(new BranchDiscoveryTrait(true, false)); + this.traits.add(new OriginPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.MERGE))); + this.traits.add(new ForkPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.MERGE), + new ForkPullRequestDiscoveryTrait.TrustTeamForks())); } @Deprecated // retained for binary compatibility public BitbucketSCMNavigator(String repoOwner, String credentialsId, String checkoutCredentialsId) { + this.serverUrl = BitbucketCloudEndpoint.SERVER_URL; this.repoOwner = repoOwner; + this.traits = new ArrayList<>(); this.credentialsId = Util.fixEmpty(credentialsId); - this.checkoutCredentialsId = checkoutCredentialsId; + // code invoking legacy constructor will want the legacy discovery model + this.traits.add(new BranchDiscoveryTrait(true, true)); + this.traits.add(new OriginPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.HEAD))); + this.traits.add(new ForkPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.HEAD), + new ForkPullRequestDiscoveryTrait.TrustEveryone())); + this.traits.add(new PublicRepoPullRequestFilterTrait()); + if (checkoutCredentialsId != null + && !BitbucketSCMSource.DescriptorImpl.SAME.equals(checkoutCredentialsId)) { + this.traits.add(new SSHCheckoutTrait(checkoutCredentialsId)); + } } + @SuppressWarnings({"ConstantConditions", "deprecation"}) + @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", + justification = "Only non-null after we set them here!") private Object readResolve() throws ObjectStreamException { - if (includes == null) { - includes = "*"; + if (serverUrl == null) { + serverUrl = BitbucketEndpointConfiguration.get().readResolveServerUrl(bitbucketServerUrl); } - if (excludes == null) { - excludes = ""; + if (traits == null) { + // legacy instance, reconstruct traits to reflect legacy behaviour + traits = new ArrayList<>(); + this.traits.add(new BranchDiscoveryTrait(true, true)); + this.traits.add(new OriginPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.HEAD))); + this.traits.add(new ForkPullRequestDiscoveryTrait( + EnumSet.of(ChangeRequestCheckoutStrategy.HEAD), + new ForkPullRequestDiscoveryTrait.TrustEveryone()) + ); + this.traits.add(new PublicRepoPullRequestFilterTrait()); + if ((includes != null && !"*".equals(includes)) || (excludes != null && !"".equals(excludes))) { + traits.add(new WildcardSCMHeadFilterTrait( + StringUtils.defaultIfBlank(includes, "*"), + StringUtils.defaultIfBlank(excludes, ""))); + } + if (checkoutCredentialsId != null + && !BitbucketSCMSource.DescriptorImpl.SAME.equals(checkoutCredentialsId)) { + traits.add(new SSHCheckoutTrait(checkoutCredentialsId)); + } + traits.add(new WebhookRegistrationTrait( + autoRegisterHooks ? WebhookRegistration.ITEM : WebhookRegistration.DISABLE) + ); + if (pattern != null && !".*".equals(pattern)) { + traits.add(new RegexSCMSourceFilterTrait(pattern)); + } } return this; } + @CheckForNull + public String getCredentialsId() { + return credentialsId; + } + + public String getRepoOwner() { + return repoOwner; + } + + @NonNull + public List> getTraits() { + return Collections.unmodifiableList(traits); + } + @DataBoundSetter - public void setCredentialsId(String credentialsId) { + public void setCredentialsId(@CheckForNull String credentialsId) { this.credentialsId = Util.fixEmpty(credentialsId); } @DataBoundSetter - public void setCheckoutCredentialsId(String checkoutCredentialsId) { - this.checkoutCredentialsId = checkoutCredentialsId; + public void setTraits(@NonNull List>> traits) { + this.traits = new ArrayList<>(/*defensive*/Util.fixNull(traits)); + } + + public String getServerUrl() { + return serverUrl; } @DataBoundSetter - public void setPattern(String pattern) { - Pattern.compile(pattern); - this.pattern = pattern; + public void setServerUrl(String serverUrl) { + serverUrl = BitbucketEndpointConfiguration.normalizeServerUrl(serverUrl); + if (!StringUtils.equals(this.serverUrl, serverUrl)) { + this.serverUrl = serverUrl; + resetId(); + } } + @Deprecated + @Restricted(DoNotUse.class) + @RestrictedSince("2.2.0") @DataBoundSetter - public void setAutoRegisterHooks(boolean autoRegisterHooks) { - this.autoRegisterHooks = autoRegisterHooks; + public void setPattern(String pattern) { + for (int i = 0; i < traits.size(); i++) { + SCMTrait trait = traits.get(i); + if (trait instanceof RegexSCMSourceFilterTrait) { + if (".*".equals(pattern)) { + traits.remove(i); + } else { + traits.set(i, new RegexSCMSourceFilterTrait(pattern)); + } + return; + } + } + if (!".*".equals(pattern)) { + traits.add(new RegexSCMSourceFilterTrait(pattern)); + } } - public String getRepoOwner() { - return repoOwner; + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + @DataBoundSetter + public void setAutoRegisterHooks(boolean autoRegisterHook) { + for (Iterator>> iterator = traits.iterator(); iterator.hasNext(); ) { + if (iterator.next() instanceof WebhookRegistrationTrait) { + iterator.remove(); + } + } + traits.add(new WebhookRegistrationTrait( + autoRegisterHook ? WebhookRegistration.ITEM : WebhookRegistration.DISABLE + )); } - @CheckForNull - public String getCredentialsId() { - return credentialsId; + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + public boolean isAutoRegisterHooks() { + for (SCMTrait> t : traits) { + if (t instanceof WebhookRegistrationTrait) { + return ((WebhookRegistrationTrait) t).getMode() != WebhookRegistration.DISABLE; + } + } + return true; } - @CheckForNull + + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + @NonNull public String getCheckoutCredentialsId() { - return checkoutCredentialsId; + for (SCMTrait t : traits) { + if (t instanceof SSHCheckoutTrait) { + return StringUtils.defaultString(((SSHCheckoutTrait) t).getCredentialsId(), BitbucketSCMSource + .DescriptorImpl.ANONYMOUS); + } + } + return BitbucketSCMSource.DescriptorImpl.SAME; } - public String getPattern() { - return pattern; + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + @DataBoundSetter + public void setCheckoutCredentialsId(String checkoutCredentialsId) { + for (Iterator> iterator = traits.iterator(); iterator.hasNext(); ) { + if (iterator.next() instanceof SSHCheckoutTrait) { + iterator.remove(); + } + } + if (checkoutCredentialsId != null && !BitbucketSCMSource.DescriptorImpl.SAME.equals(checkoutCredentialsId)) { + traits.add(new SSHCheckoutTrait(checkoutCredentialsId)); + } } - public boolean isAutoRegisterHooks() { - return autoRegisterHooks; + @Deprecated + @Restricted(DoNotUse.class) + @RestrictedSince("2.2.0") + public String getPattern() { + for (SCMTrait trait : traits) { + if (trait instanceof RegexSCMSourceFilterTrait) { + return ((RegexSCMSourceFilterTrait) trait).getRegex(); + } + } + return ".*"; } + @Deprecated + @Restricted(DoNotUse.class) + @RestrictedSince("2.2.0") @DataBoundSetter public void setBitbucketServerUrl(String url) { - if (StringUtils.equals(this.bitbucketServerUrl, url)) { + url = BitbucketEndpointConfiguration.normalizeServerUrl(url); + AbstractBitbucketEndpoint endpoint = BitbucketEndpointConfiguration.get().findEndpoint(url); + if (endpoint != null) { + // we have a match + setServerUrl(url); return; } - this.bitbucketServerUrl = Util.fixEmpty(url); - if (this.bitbucketServerUrl != null) { - // Remove a possible trailing slash - this.bitbucketServerUrl = this.bitbucketServerUrl.replaceAll("/$", ""); - } - resetId(); + LOGGER.log(Level.WARNING, "Call to legacy setBitbucketServerUrl({0}) method is configuring an url missing " + + "from the global configuration.", url); + setServerUrl(url); } + @Deprecated + @Restricted(DoNotUse.class) + @RestrictedSince("2.2.0") @CheckForNull public String getBitbucketServerUrl() { - return bitbucketServerUrl; + if (BitbucketEndpointConfiguration.get().findEndpoint(serverUrl) instanceof BitbucketCloudEndpoint) { + return null; + } + return serverUrl; } + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + @NonNull public String getIncludes() { - return includes; + for (SCMTrait trait : traits) { + if (trait instanceof WildcardSCMHeadFilterTrait) { + return ((WildcardSCMHeadFilterTrait) trait).getIncludes(); + } + } + return "*"; } + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") @DataBoundSetter - public void setIncludes(String includes) { - this.includes = includes; + public void setIncludes(@NonNull String includes) { + for (int i = 0; i < traits.size(); i++) { + SCMTrait trait = traits.get(i); + if (trait instanceof WildcardSCMHeadFilterTrait) { + WildcardSCMHeadFilterTrait existing = (WildcardSCMHeadFilterTrait) trait; + if ("*".equals(includes) && "".equals(existing.getExcludes())) { + traits.remove(i); + } else { + traits.set(i, new WildcardSCMHeadFilterTrait(includes, existing.getExcludes())); + } + return; + } + } + if (!"*".equals(includes)) { + traits.add(new WildcardSCMHeadFilterTrait(includes, "")); + } } + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + @NonNull public String getExcludes() { - return excludes; + for (SCMTrait trait : traits) { + if (trait instanceof WildcardSCMHeadFilterTrait) { + return ((WildcardSCMHeadFilterTrait) trait).getExcludes(); + } + } + return ""; } + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") @DataBoundSetter - public void setExcludes(String excludes) { - this.excludes = excludes; + public void setExcludes(@NonNull String excludes) { + for (int i = 0; i < traits.size(); i++) { + SCMTrait trait = traits.get(i); + if (trait instanceof WildcardSCMHeadFilterTrait) { + WildcardSCMHeadFilterTrait existing = (WildcardSCMHeadFilterTrait) trait; + if ("*".equals(existing.getIncludes()) && "".equals(excludes)) { + traits.remove(i); + } else { + traits.set(i, new WildcardSCMHeadFilterTrait(existing.getIncludes(), excludes)); + } + return; + } + } + if (!"".equals(excludes)) { + traits.add(new WildcardSCMHeadFilterTrait("*", excludes)); + } } + @NonNull @Override protected String id() { - return bitbucketUrl() + "::" + repoOwner; + return serverUrl + "::" + repoOwner; } @Override @@ -202,64 +449,46 @@ public void visitSources(SCMSourceObserver observer) throws IOException, Interru return; } StandardUsernamePasswordCredentials credentials = BitbucketCredentials.lookupCredentials( - bitbucketServerUrl, + serverUrl, observer.getContext(), credentialsId, StandardUsernamePasswordCredentials.class ); if (credentials == null) { - listener.getLogger().format("Connecting to %s with no credentials, anonymous access%n", bitbucketUrl()); + listener.getLogger().format("Connecting to %s with no credentials, anonymous access%n", serverUrl); } else { - listener.getLogger().format("Connecting to %s using %s%n", bitbucketUrl(), CredentialsNameProvider.name(credentials)); + listener.getLogger() + .format("Connecting to %s using %s%n", serverUrl, CredentialsNameProvider.name(credentials)); } - List repositories; - BitbucketApi bitbucket = BitbucketApiFactory.newInstance(bitbucketServerUrl, credentials, repoOwner, null); - BitbucketTeam team = bitbucket.getTeam(); - if (team != null) { - // Navigate repositories of the team - listener.getLogger().format("Looking up repositories of team %s%n", repoOwner); - repositories = bitbucket.getRepositories(); - } else { - // Navigate the repositories of the repoOwner as a user - listener.getLogger().format("Looking up repositories of user %s%n", repoOwner); - repositories = bitbucket.getRepositories(UserRoleInRepository.OWNER); - } - for (BitbucketRepository repo : repositories) { - checkInterrupt(); - add(listener, observer, repo); + try (final BitbucketSCMNavigatorRequest request = new BitbucketSCMNavigatorContext().withTraits(traits) + .newRequest(this, observer)) { + SourceFactory sourceFactory = new SourceFactory(request); + WitnessImpl witness = new WitnessImpl(listener); + + BitbucketApi bitbucket = BitbucketApiFactory.newInstance(serverUrl, credentials, repoOwner, null); + BitbucketTeam team = bitbucket.getTeam(); + List repositories; + if (team != null) { + // Navigate repositories of the team + listener.getLogger().format("Looking up repositories of team %s%n", repoOwner); + repositories = bitbucket.getRepositories(); + } else { + // Navigate the repositories of the repoOwner as a user + listener.getLogger().format("Looking up repositories of user %s%n", repoOwner); + repositories = bitbucket.getRepositories(UserRoleInRepository.OWNER); + } + for (BitbucketRepository repo : repositories) { + if (request.process(repo.getRepositoryName(), sourceFactory, null, witness)) { + listener.getLogger().format( + "%d repositories were processed (query completed)%n", witness.getCount() + ); + } + } + listener.getLogger().format("%d repositories were processed%n", witness.getCount()); } } - private void add(TaskListener listener, SCMSourceObserver observer, BitbucketRepository repo) - throws InterruptedException, IOException { - String name = repo.getRepositoryName(); - if (!Pattern.compile(pattern).matcher(name).matches()) { - listener.getLogger().format("Ignoring %s%n", name); - return; - } - listener.getLogger().format("Proposing %s%n", name); - checkInterrupt(); - SCMSourceObserver.ProjectObserver projectObserver = observer.observe(name); - BitbucketSCMSource scmSource = new BitbucketSCMSource( - getId() + "::" + name, - repoOwner, - name - ); - scmSource.setCredentialsId(credentialsId); - scmSource.setCheckoutCredentialsId(checkoutCredentialsId); - scmSource.setAutoRegisterHook(isAutoRegisterHooks()); - scmSource.setBitbucketServerUrl(bitbucketServerUrl); - scmSource.setIncludes(includes); - scmSource.setExcludes(excludes); - projectObserver.addSource(scmSource); - projectObserver.complete(); - } - - private String bitbucketUrl() { - return StringUtils.defaultIfBlank(bitbucketServerUrl, "https://bitbucket.org"); - } - @NonNull @Override public List retrieveActions(@NonNull SCMNavigatorOwner owner, @@ -270,13 +499,12 @@ public List retrieveActions(@NonNull SCMNavigatorOwner owner, listener.getLogger().printf("Looking up team details of %s...%n", getRepoOwner()); List result = new ArrayList<>(); StandardUsernamePasswordCredentials credentials = BitbucketCredentials.lookupCredentials( - bitbucketServerUrl, + serverUrl, owner, credentialsId, StandardUsernamePasswordCredentials.class ); - String serverUrl = StringUtils.removeEnd(bitbucketUrl(), "/"); if (credentials == null) { listener.getLogger().format("Connecting to %s with no credentials, anonymous access%n", serverUrl); @@ -285,7 +513,7 @@ public List retrieveActions(@NonNull SCMNavigatorOwner owner, serverUrl, CredentialsNameProvider.name(credentials)); } - BitbucketApi bitbucket = BitbucketApiFactory.newInstance(bitbucketServerUrl, credentials, repoOwner, null); + BitbucketApi bitbucket = BitbucketApiFactory.newInstance(serverUrl, credentials, repoOwner, null); BitbucketTeam team = bitbucket.getTeam(); if (team != null) { String teamUrl = @@ -353,7 +581,15 @@ public String getIconClassName() { @Override public SCMNavigator newInstance(String name) { - return new BitbucketSCMNavigator(name, "", BitbucketSCMSource.DescriptorImpl.SAME); + return new BitbucketSCMNavigator(StringUtils.defaultString(name)); + } + + public boolean isServerUrlSelectable() { + return BitbucketEndpointConfiguration.get().isEndpointSelectable(); + } + + public ListBoxModel doFillServerUrlItems() { + return BitbucketEndpointConfiguration.get().getEndpointItems(); } public FormValidation doCheckCredentialsId(@QueryParameter String value) { @@ -364,28 +600,118 @@ public FormValidation doCheckCredentialsId(@QueryParameter String value) { } } + @Restricted(DoNotUse.class) + @Deprecated public FormValidation doCheckBitbucketServerUrl(@QueryParameter String bitbucketServerUrl) { return BitbucketSCMSource.DescriptorImpl.doCheckBitbucketServerUrl(bitbucketServerUrl); } - public ListBoxModel doFillCredentialsIdItems(@AncestorInPath SCMSourceOwner context, @QueryParameter String bitbucketServerUrl) { + public static FormValidation doCheckServerUrl(@QueryParameter String value) { + if (BitbucketEndpointConfiguration.get().findEndpoint(value) == null) { + return FormValidation.error("Unregistered Server: " + value); + } + return FormValidation.ok(); + } + + + public ListBoxModel doFillCredentialsIdItems(@AncestorInPath SCMSourceOwner context, + @QueryParameter String bitbucketServerUrl) { StandardListBoxModel result = new StandardListBoxModel(); result.withEmptySelection(); - return BitbucketCredentials.fillCredentials(bitbucketServerUrl, context, result); + result.includeMatchingAs( + context instanceof Queue.Task + ? Tasks.getDefaultAuthenticationOf((Queue.Task) context) + : ACL.SYSTEM, + context, + StandardUsernameCredentials.class, + URIRequirementBuilder.fromUri(bitbucketServerUrl).build(), + CredentialsMatchers.anyOf(CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class)) + ); + return result; } - public ListBoxModel doFillCheckoutCredentialsIdItems(@AncestorInPath SCMSourceOwner context, @QueryParameter String bitbucketServerUrl) { + public List>> getTraitsDescriptorLists() { + List> all = new ArrayList<>(); + all.addAll( + SCMNavigatorTrait._for(this, BitbucketSCMNavigatorContext.class, BitbucketSCMSourceBuilder.class)); + all.addAll(SCMSourceTrait._for(BitbucketSCMSourceContext.class, null)); + all.addAll(SCMSourceTrait._for(null, BitbucketGitSCMBuilder.class)); + all.addAll(SCMSourceTrait._for(null, BitbucketHgSCMBuilder.class)); + Set> dedup = new HashSet<>(); + for (Iterator> iterator = all.iterator(); iterator.hasNext(); ) { + SCMTraitDescriptor d = iterator.next(); + if (dedup.contains(d) + || d instanceof MercurialBrowserSCMSourceTrait.DescriptorImpl + || d instanceof GitBrowserSCMSourceTrait.DescriptorImpl) { + // remove any we have seen already and ban the browser configuration as it will always be bitbucket + iterator.remove(); + } else { + dedup.add(d); + } + } + List>> result = new ArrayList<>(); + NamedArrayList.select(all, "Repositories", new NamedArrayList.Predicate>() { + @Override + public boolean test(SCMTraitDescriptor scmTraitDescriptor) { + return scmTraitDescriptor instanceof SCMNavigatorTraitDescriptor; + } + }, + true, result); + NamedArrayList.select(all, "Within repository", NamedArrayList + .anyOf(NamedArrayList.withAnnotation(Discovery.class), + NamedArrayList.withAnnotation(Selection.class)), + true, result); + int insertionPoint = result.size(); + NamedArrayList.select(all, "Git", new NamedArrayList.Predicate>() { + @Override + public boolean test(SCMTraitDescriptor d) { + return GitSCM.class.isAssignableFrom(d.getScmClass()); + } + }, true, result); + NamedArrayList.select(all, "Mercurial", new NamedArrayList.Predicate>() { + @Override + public boolean test(SCMTraitDescriptor d) { + return MercurialSCM.class.isAssignableFrom(d.getScmClass()); + } + }, true, result); + NamedArrayList.select(all, "Additional", null, true, result, insertionPoint); + return result; + } + + public List> getTraitsDefaults() { + return Arrays.>asList( + new BranchDiscoveryTrait(true, false), + new OriginPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.MERGE)), + new ForkPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.MERGE), + new ForkPullRequestDiscoveryTrait.TrustTeamForks()) + ); + } + + @Restricted(DoNotUse.class) + @Deprecated + public ListBoxModel doFillCheckoutCredentialsIdItems(@AncestorInPath SCMSourceOwner context, + @QueryParameter String bitbucketServerUrl) { StandardListBoxModel result = new StandardListBoxModel(); result.add("- same as scan credentials -", BitbucketSCMSource.DescriptorImpl.SAME); result.add("- anonymous -", BitbucketSCMSource.DescriptorImpl.ANONYMOUS); - return BitbucketCredentials.fillCheckoutCredentials(bitbucketServerUrl, context, result); + result.includeMatchingAs( + context instanceof Queue.Task + ? Tasks.getDefaultAuthenticationOf((Queue.Task) context) + : ACL.SYSTEM, + context, + StandardCredentials.class, + URIRequirementBuilder.fromUri(bitbucketServerUrl).build(), + CredentialsMatchers.anyOf(CredentialsMatchers.instanceOf(StandardCredentials.class)) + ); + return result; } @NonNull @Override protected SCMSourceCategory[] createCategories() { return new SCMSourceCategory[]{ - new UncategorizedSCMSourceCategory(Messages._BitbucketSCMNavigator_UncategorizedSCMSourceCategory_DisplayName()) + new UncategorizedSCMSourceCategory( + Messages._BitbucketSCMNavigator_UncategorizedSCMSourceCategory_DisplayName()) }; } @@ -493,4 +819,48 @@ protected SCMSourceCategory[] createCategories() { Icon.ICON_XLARGE_STYLE)); } } + + private static class WitnessImpl implements SCMNavigatorRequest.Witness { + private int count; + private final TaskListener listener; + + public WitnessImpl(TaskListener listener) { + this.listener = listener; + } + + @Override + public void record(@NonNull String name, boolean isMatch) { + if (isMatch) { + listener.getLogger().format("Proposing %s%n", name); + count++; + } else { + listener.getLogger().format("Ignoring %s%n", name); + } + } + + public int getCount() { + return count; + } + } + + private class SourceFactory implements SCMNavigatorRequest.SourceLambda { + private final BitbucketSCMNavigatorRequest request; + + public SourceFactory(BitbucketSCMNavigatorRequest request) { + this.request = request; + } + + @NonNull + @Override + public SCMSource create(@NonNull String projectName) throws IOException, InterruptedException { + return new BitbucketSCMSourceBuilder( + getId() + "::" + projectName, + serverUrl, + credentialsId, + repoOwner, + projectName) + .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 8b698979e..d05d0ddb8 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java @@ -33,49 +33,60 @@ 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.sshcredentials.SSHUserPrivateKey; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketTeam; +import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudApiClient; +import com.cloudbees.jenkins.plugins.bitbucket.client.repository.UserRoleInRepository; +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.plugins.credentials.CredentialsMatchers; import com.cloudbees.plugins.credentials.CredentialsNameProvider; import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.cloudbees.plugins.credentials.common.StandardListBoxModel; +import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; +import hudson.RestrictedSince; import hudson.Util; +import hudson.console.HyperlinkNote; import hudson.model.Action; import hudson.model.Actionable; +import hudson.model.Item; +import hudson.model.Queue; import hudson.model.TaskListener; -import hudson.plugins.git.BranchSpec; +import hudson.model.queue.Tasks; import hudson.plugins.git.GitSCM; -import hudson.plugins.git.SubmoduleConfig; -import hudson.plugins.git.UserRemoteConfig; -import hudson.plugins.git.extensions.GitSCMExtension; -import hudson.plugins.git.extensions.impl.BuildChooserSetting; -import hudson.plugins.git.util.BuildChooser; -import hudson.plugins.git.util.DefaultBuildChooser; import hudson.plugins.mercurial.MercurialSCM; -import hudson.plugins.mercurial.MercurialSCM.RevisionType; +import hudson.plugins.mercurial.traits.MercurialBrowserSCMSourceTrait; import hudson.scm.SCM; +import hudson.security.ACL; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import java.io.IOException; +import java.io.ObjectStreamException; import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.EnumSet; import java.util.HashSet; +import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.regex.Pattern; -import jenkins.plugins.git.AbstractGitSCMSource; -import jenkins.plugins.git.AbstractGitSCMSource.SpecificRevisionBuildChooser; +import jenkins.plugins.git.AbstractGitSCMSource.SCMRevisionImpl; +import jenkins.plugins.git.traits.GitBrowserSCMSourceTrait; import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMHeadCategory; import jenkins.scm.api.SCMHeadEvent; @@ -90,12 +101,23 @@ import jenkins.scm.api.metadata.ContributorMetadataAction; import jenkins.scm.api.metadata.ObjectMetadataAction; import jenkins.scm.api.metadata.PrimaryInstanceMetadataAction; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; +import jenkins.scm.api.trait.SCMSourceRequest; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMSourceTraitDescriptor; import jenkins.scm.impl.ChangeRequestSCMHeadCategory; import jenkins.scm.impl.UncategorizedSCMHeadCategory; +import jenkins.scm.impl.form.NamedArrayList; +import jenkins.scm.impl.trait.Discovery; +import jenkins.scm.impl.trait.Selection; +import jenkins.scm.impl.trait.WildcardSCMHeadFilterTrait; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.WordUtils; import org.eclipse.jgit.lib.Constants; import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.DoNotUse; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; @@ -109,53 +131,86 @@ */ public class BitbucketSCMSource extends SCMSource { + private static final Logger LOGGER = Logger.getLogger(BitbucketSCMSource.class.getName()); + /** - * Credentials used to access the Bitbucket REST API. + * Bitbucket URL. */ - private String credentialsId; + @NonNull + private String serverUrl = BitbucketCloudEndpoint.SERVER_URL; /** - * Credentials used to clone the repository/repositories. + * Credentials used to access the Bitbucket REST API. */ - private String checkoutCredentialsId; + @CheckForNull + private String credentialsId; /** * Repository owner. * Used to build the repository URL. */ + @NonNull private final String repoOwner; /** * Repository name. * Used to build the repository URL. */ + @NonNull private final String repository; + /** + * The behaviours to apply to this source. + */ + @NonNull + private List traits; + + /** + * Credentials used to clone the repository/repositories. + */ + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + private transient String checkoutCredentialsId; + /** * Ant match expression that indicates what branches to include in the retrieve process. */ - private String includes = "*"; + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + private transient String includes; /** * Ant match expression that indicates what branches to exclude in the retrieve process. */ - private String excludes = ""; + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + private transient String excludes; /** * If true, a webhook will be auto-registered in the repository managed by this source. */ - private boolean autoRegisterHook = false; + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + private transient boolean autoRegisterHook; /** * Bitbucket Server URL. * An specific HTTP client is used if this field is not null. */ - private String bitbucketServerUrl; + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + private transient String bitbucketServerUrl; /** - * Repository type. + * The cache of the repository type. */ - private BitbucketRepositoryType repositoryType; + @CheckForNull + private transient BitbucketRepositoryType repositoryType; /** * The cache of pull request titles for each open PR. @@ -167,16 +222,63 @@ public class BitbucketSCMSource extends SCMSource { */ @CheckForNull private transient /*effectively final*/ Map pullRequestContributorCache; + /** + * The cache of the clone links. + */ @CheckForNull private transient List cloneLinks = null; - private static final Logger LOGGER = Logger.getLogger(BitbucketSCMSource.class.getName()); - + /** + * Constructor. + * + * @param id the id. + * @param repoOwner the repository owner. + * @param repository the repository name. + */ @DataBoundConstructor - public BitbucketSCMSource(String id, String repoOwner, String repository) { + public BitbucketSCMSource(@CheckForNull String id, @NonNull String repoOwner, @NonNull String repository) { super(id); + this.serverUrl = BitbucketCloudEndpoint.SERVER_URL; this.repoOwner = repoOwner; this.repository = repository; + this.traits = new ArrayList<>(); + this.traits.add(new BranchDiscoveryTrait(true, true)); + traits.add(new OriginPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.MERGE))); + traits.add(new ForkPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.MERGE), + new ForkPullRequestDiscoveryTrait.TrustTeamForks())); + } + + /** + * Migrate legacy serialization formats. + * + * @return {@code this} + * @throws ObjectStreamException if things go wrong. + */ + @SuppressWarnings("ConstantConditions") + @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", + justification = "Only non-null after we set them here!") + private Object readResolve() throws ObjectStreamException { + if (serverUrl == null) { + serverUrl = BitbucketEndpointConfiguration.get().readResolveServerUrl(bitbucketServerUrl); + } + if (traits == null) { + traits = new ArrayList<>(); + if (!"*".equals(includes) || !"".equals(excludes)) { + traits.add(new WildcardSCMHeadFilterTrait(includes, excludes)); + } + if (!DescriptorImpl.SAME.equals(checkoutCredentialsId)) { + traits.add(new SSHCheckoutTrait(checkoutCredentialsId)); + } + traits.add(new WebhookRegistrationTrait( + autoRegisterHook ? WebhookRegistration.ITEM : WebhookRegistration.DISABLE) + ); + traits.add(new BranchDiscoveryTrait(true, true)); + traits.add(new OriginPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.HEAD))); + traits.add(new ForkPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.HEAD), + new ForkPullRequestDiscoveryTrait.TrustEveryone())); + traits.add(new PublicRepoPullRequestFilterTrait()); + } + return this; } @CheckForNull @@ -185,103 +287,193 @@ public String getCredentialsId() { } @DataBoundSetter - public void setCredentialsId(String credentialsId) { + public void setCredentialsId(@CheckForNull String credentialsId) { this.credentialsId = Util.fixEmpty(credentialsId); } - @CheckForNull - public String getCheckoutCredentialsId() { - return checkoutCredentialsId; + @NonNull + public String getRepoOwner() { + return repoOwner; } - @DataBoundSetter - public void setCheckoutCredentialsId(String checkoutCredentialsId) { - this.checkoutCredentialsId = checkoutCredentialsId; + @NonNull + public String getRepository() { + return repository; } - public String getIncludes() { - return includes; + @NonNull + public String getServerUrl() { + return serverUrl; } @DataBoundSetter - public void setIncludes(@NonNull String includes) { - Pattern.compile(getPattern(includes)); - this.includes = includes; + public void setServerUrl(@CheckForNull String serverUrl) { + this.serverUrl = BitbucketEndpointConfiguration.normalizeServerUrl(serverUrl); } - public String getExcludes() { - return excludes; + @NonNull + public List getTraits() { + return Collections.unmodifiableList(traits); } @DataBoundSetter - public void setExcludes(@NonNull String excludes) { - Pattern.compile(getPattern(excludes)); - this.excludes = excludes; + public void setTraits(@CheckForNull List traits) { + this.traits = new ArrayList<>(Util.fixNull(traits)); } - public String getRepoOwner() { - return repoOwner; + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + @DataBoundSetter + public void setBitbucketServerUrl(String url) { + url = BitbucketEndpointConfiguration.normalizeServerUrl(url); + AbstractBitbucketEndpoint endpoint = BitbucketEndpointConfiguration.get().findEndpoint(url); + if (endpoint != null) { + // we have a match + setServerUrl(endpoint.getServerUrl()); + return; + } + LOGGER.log(Level.WARNING, "Call to legacy setBitbucketServerUrl({0}) method is configuring an url missing " + + "from the global configuration.", url); + setServerUrl(url); } - public String getRepository() { - return repository; + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + @CheckForNull + public String getBitbucketServerUrl() { + String serverUrl = getServerUrl(); + if (BitbucketEndpointConfiguration.get().findEndpoint(serverUrl) instanceof BitbucketCloudEndpoint) { + return null; + } + return serverUrl; + } + + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + @CheckForNull + public String getCheckoutCredentialsId() { + for (SCMSourceTrait t : traits) { + if (t instanceof SSHCheckoutTrait) { + return StringUtils.defaultString(((SSHCheckoutTrait) t).getCredentialsId(), DescriptorImpl.ANONYMOUS); + } + } + return DescriptorImpl.SAME; } + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") @DataBoundSetter - public void setAutoRegisterHook(boolean autoRegisterHook) { - this.autoRegisterHook = autoRegisterHook; + public void setCheckoutCredentialsId(String checkoutCredentialsId) { + for (Iterator iterator = traits.iterator(); iterator.hasNext(); ) { + if (iterator.next() instanceof SSHCheckoutTrait) { + iterator.remove(); + } + } + if (checkoutCredentialsId != null && !DescriptorImpl.SAME.equals(checkoutCredentialsId)) { + traits.add(new SSHCheckoutTrait(checkoutCredentialsId)); + } } - public boolean isAutoRegisterHook() { - return autoRegisterHook; + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + @NonNull + public String getIncludes() { + for (SCMSourceTrait trait : traits) { + if (trait instanceof WildcardSCMHeadFilterTrait) { + return ((WildcardSCMHeadFilterTrait) trait).getIncludes(); + } + } + return "*"; } + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") @DataBoundSetter - public void setBitbucketServerUrl(String url) { - this.bitbucketServerUrl = Util.fixEmpty(url); - if (this.bitbucketServerUrl != null) { - // Remove a possible trailing slash - this.bitbucketServerUrl = this.bitbucketServerUrl.replaceAll("/$", ""); + public void setIncludes(@NonNull String includes) { + for (int i = 0; i < traits.size(); i++) { + SCMSourceTrait trait = traits.get(i); + if (trait instanceof WildcardSCMHeadFilterTrait) { + WildcardSCMHeadFilterTrait existing = (WildcardSCMHeadFilterTrait) trait; + if ("*".equals(includes) && "".equals(existing.getExcludes())) { + traits.remove(i); + } else { + traits.set(i, new WildcardSCMHeadFilterTrait(includes, existing.getExcludes())); + } + return; + } + } + if (!"*".equals(includes)) { + traits.add(new WildcardSCMHeadFilterTrait(includes, "")); } } - @CheckForNull - public String getBitbucketServerUrl() { - return bitbucketServerUrl; + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + @NonNull + public String getExcludes() { + for (SCMSourceTrait trait : traits) { + if (trait instanceof WildcardSCMHeadFilterTrait) { + return ((WildcardSCMHeadFilterTrait) trait).getExcludes(); + } + } + return ""; } - private String bitbucketUrl() { - return StringUtils.defaultIfBlank(bitbucketServerUrl, "https://bitbucket.org"); + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + @DataBoundSetter + public void setExcludes(@NonNull String excludes) { + for (int i = 0; i < traits.size(); i++) { + SCMSourceTrait trait = traits.get(i); + if (trait instanceof WildcardSCMHeadFilterTrait) { + WildcardSCMHeadFilterTrait existing = (WildcardSCMHeadFilterTrait) trait; + if ("*".equals(existing.getIncludes()) && "".equals(excludes)) { + traits.remove(i); + } else { + traits.set(i, new WildcardSCMHeadFilterTrait(existing.getIncludes(), excludes)); + } + return; + } + } + if (!"".equals(excludes)) { + traits.add(new WildcardSCMHeadFilterTrait("*", excludes)); + } } - public String getRemote(@NonNull String repoOwner, @NonNull String repository, BitbucketRepositoryType repositoryType) { - assert repositoryType != null; - BitbucketRepositoryProtocol protocol; - Integer protocolPortOverride = null; - if (StringUtils.isBlank(checkoutCredentialsId)) { - protocol = BitbucketRepositoryProtocol.HTTP; - } else if (getCheckoutCredentials() instanceof SSHUserPrivateKey) { - protocol = BitbucketRepositoryProtocol.SSH; - if (cloneLinks != null) { - for (BitbucketHref link : cloneLinks) { - if ("ssh".equals(link.getName())) { - // extract the port from this link and use that - try { - URI uri = new URI(link.getHref()); - if (uri.getPort() != -1) { - protocolPortOverride = uri.getPort(); - } - } catch (URISyntaxException e) { - // ignore - } - break; - } - } + + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + @DataBoundSetter + public void setAutoRegisterHook(boolean autoRegisterHook) { + for (Iterator iterator = traits.iterator(); iterator.hasNext(); ) { + if (iterator.next() instanceof WebhookRegistrationTrait) { + iterator.remove(); + } + } + traits.add(new WebhookRegistrationTrait( + autoRegisterHook ? WebhookRegistration.ITEM : WebhookRegistration.DISABLE + )); + } + + @Deprecated + @Restricted(NoExternalUse.class) + @RestrictedSince("2.2.0") + public boolean isAutoRegisterHook() { + for (SCMSourceTrait t : traits) { + if (t instanceof WebhookRegistrationTrait) { + return ((WebhookRegistrationTrait) t).getMode() != WebhookRegistration.DISABLE; } - } else { - protocol = BitbucketRepositoryProtocol.HTTP; } - return buildBitbucketClient().getRepositoryUri(repositoryType, protocol, protocolPortOverride, repoOwner, repository); + return true; } public BitbucketRepositoryType getRepositoryType() throws IOException, InterruptedException { @@ -297,11 +489,11 @@ public BitbucketRepositoryType getRepositoryType() throws IOException, Interrupt } public BitbucketApi buildBitbucketClient() { - return BitbucketApiFactory.newInstance(bitbucketServerUrl, getScanCredentials(), repoOwner, repository); + return BitbucketApiFactory.newInstance(getServerUrl(), credentials(), repoOwner, repository); } public BitbucketApi buildBitbucketClient(PullRequestSCMHead head) { - return BitbucketApiFactory.newInstance(bitbucketServerUrl, getScanCredentials(), head.getRepoOwner(), head.getRepository()); + return BitbucketApiFactory.newInstance(getServerUrl(), credentials(), head.getRepoOwner(), head.getRepository()); } @Override @@ -311,8 +503,7 @@ public void afterSave() { } catch (InterruptedException | IOException e) { LOGGER.log(Level.FINE, "Could not determine repository type of " + getRepoOwner() + "/" + getRepository() + " on " - + StringUtils.defaultIfBlank(getBitbucketServerUrl(), "bitbucket.org") + " for " - + getOwner(), e); + + getServerUrl() + " for " + getOwner(), e); } } @@ -320,243 +511,346 @@ public void afterSave() { protected void retrieve(@CheckForNull SCMSourceCriteria criteria, @NonNull SCMHeadObserver observer, @CheckForNull SCMHeadEvent event, @NonNull TaskListener listener) throws IOException, InterruptedException { - if (event != null) { - observer = event.filter(this, observer); - } - StandardUsernamePasswordCredentials scanCredentials = getScanCredentials(); - if (scanCredentials == null) { - listener.getLogger().format("Connecting to %s with no credentials, anonymous access%n", bitbucketUrl()); - } else { - listener.getLogger().format("Connecting to %s using %s%n", bitbucketUrl(), CredentialsNameProvider.name(scanCredentials)); - } - // this has the side-effect of ensuring that repository type is always populated. - listener.getLogger().format("Repository type: %s%n", WordUtils.capitalizeFully(getRepositoryType().name())); + try (BitbucketSCMSourceRequest request = new BitbucketSCMSourceContext(criteria, observer) + .withTraits(traits) + .newRequest(this, listener)) { + StandardUsernamePasswordCredentials scanCredentials = credentials(); + if (scanCredentials == null) { + listener.getLogger().format("Connecting to %s with no credentials, anonymous access%n", getServerUrl()); + } else { + listener.getLogger().format("Connecting to %s using %s%n", getServerUrl(), + CredentialsNameProvider.name(scanCredentials)); + } + // this has the side-effect of ensuring that repository type is always populated. + listener.getLogger().format("Repository type: %s%n", WordUtils.capitalizeFully(getRepositoryType().name())); + // populate the request with its data sources + if (request.isFetchPRs()) { + request.setPullRequests(new LazyIterable() { + @Override + protected Iterable create() { + try { + return (Iterable) buildBitbucketClient().getPullRequests(); + } catch (IOException | InterruptedException e) { + throw new BitbucketSCMSource.WrappedException(e); + } + } + }); + } + if (request.isFetchBranches()) { + request.setBranches(new LazyIterable() { + @Override + protected Iterable create() { + try { + return (Iterable) buildBitbucketClient().getBranches(); + } catch (IOException | InterruptedException e) { + throw new BitbucketSCMSource.WrappedException(e); + } + } + }); + } + if (request.isFetchTags()) { + // TODO request.setTags(...); + } - // Search branches - retrieveBranches(criteria, observer, listener); - // Search pull requests - retrievePullRequests(criteria, observer, listener); + // now server the request + if (request.isFetchBranches() && !request.isComplete()) { + // Search branches + retrieveBranches(request); + } + if (request.isFetchPRs() && !request.isComplete()) { + // Search pull requests + retrievePullRequests(request); + } + if (request.isFetchTags() && !request.isComplete()) { + // TODO + } + } catch (WrappedException e) { + e.unwrap(); + } } - private void retrievePullRequests(SCMSourceCriteria criteria, SCMHeadObserver observer, final TaskListener listener) + private void retrievePullRequests(final BitbucketSCMSourceRequest request) throws IOException, InterruptedException { - String fullName = repoOwner + "/" + repository; - listener.getLogger().println("Looking up " + fullName + " for pull requests"); + final String fullName = repoOwner + "/" + repository; - final BitbucketApi bitbucket = buildBitbucketClient(); - if (bitbucket.isPrivate()) { - List pulls = bitbucket.getPullRequests(); - Set livePRs = new HashSet<>(); - Set includes = observer.getIncludes(); - for (final BitbucketPullRequest pull : pulls) { - checkInterrupt(); - PullRequestSCMHead head = new PullRequestSCMHead(pull.getSource().getRepository().getOwnerName(), - pull.getSource().getRepository().getRepositoryName(), repositoryType, - pull.getSource().getBranch().getName(), pull, - getRepoOwner().equalsIgnoreCase(pull.getSource().getRepository().getOwnerName()) - ? SCMHeadOrigin.DEFAULT : new SCMHeadOrigin.Fork( - pull.getSource().getRepository().getOwnerName()) - ); - if (includes != null && !includes.contains(head)) { - continue; - } - - - listener.getLogger().println( - "Checking PR from " + pull.getSource().getRepository().getFullName() + " and branch " - + pull.getSource().getBranch().getName()); + class Skip extends IOException { + } - // Resolve full hash. See https://bitbucket.org/site/master/issues/11415/pull-request-api-should-return-full-commit + final BitbucketApi originBitbucket = buildBitbucketClient(); + if (request.isSkipPublicPRs() && !originBitbucket.isPrivate()) { + request.listener().getLogger().printf("Skipping pull requests for %s (public repository)%n", fullName); + return; + } - String hash; - try { - hash = bitbucket.resolveSourceFullHash(pull); - } catch (BitbucketRequestException e) { - if (e.getHttpCode() == 403) { - listener.getLogger().println( - "Do not have permission to view PR from " + pull.getSource().getRepository().getFullName() + " and branch " - + pull.getSource().getBranch().getName()); - // the credentials do not have permission, so we should not observe the PR ever - // the PR is dead to us, so this is the one case where we can squash the exception. - continue; + request.listener().getLogger().printf("Looking up %s for pull requests%n", fullName); + final Set livePRs = new HashSet<>(); + int count = 0; + Map> strategies = request.getPRStrategies(); + for (final BitbucketPullRequest pull : request.getPullRequests()) { + request.listener().getLogger().printf( + "Checking PR-%s from %s and branch %s%n", + pull.getId(), + pull.getSource().getRepository().getFullName(), + pull.getSource().getBranch().getName() + ); + boolean fork = !fullName.equalsIgnoreCase(pull.getSource().getRepository().getFullName()); + String pullRepoOwner = pull.getSource().getRepository().getOwnerName(); + String pullRepository = pull.getSource().getRepository().getRepositoryName(); + final BitbucketApi pullBitbucket = fork && originBitbucket instanceof BitbucketCloudApiClient + ? BitbucketApiFactory.newInstance( + getServerUrl(), + credentials(), + pullRepoOwner, + pullRepository + ) + : originBitbucket; + count++; + livePRs.add(pull.getId()); + getPullRequestTitleCache() + .put(pull.getId(), StringUtils.defaultString(pull.getTitle())); + getPullRequestContributorCache().put(pull.getId(), + // TODO get more details on the author + new ContributorMetadataAction(pull.getAuthorLogin(), null, null) + ); + try { + // We store resolved hashes here so to avoid resolving the commits multiple times + for (final ChangeRequestCheckoutStrategy strategy : strategies.get(fork)) { + final String branchName; + if (strategies.get(fork).size() == 1) { + branchName = "PR-" + pull.getId(); } else { - // this is some other unexpected error, we need to abort observing, so throw. - throw e; + branchName = "PR-" + pull.getId() + "-" + strategy.name().toLowerCase(Locale.ENGLISH); + } + if (request.process( + new PullRequestSCMHead(branchName, + pullRepoOwner, + pullRepository, + repositoryType, + pull.getSource().getBranch().getName(), + pull, + originOf(pullRepoOwner, pullRepository), + strategy + ), + new SCMSourceRequest.IntermediateLambda() { + @Nullable + @Override + public String create() throws IOException, InterruptedException { + try { + return originBitbucket.resolveSourceFullHash(pull); + } catch (BitbucketRequestException e) { + if (originBitbucket instanceof BitbucketCloudApiClient) { + if (e.getHttpCode() == 403) { + request.listener().getLogger().printf("Skipping %s because of %s%n", + pull.getId(), HyperlinkNote.encodeTo( + "https://bitbucket.org/site/master" + + "/issues/5814/reify-pull-requests" + + "-by-making-them-a-ref", + "a permission issue accessing pull requests " + + "from forks")); + throw new Skip(); + } + } + // https://bitbucket + // .org/site/master/issues/5814/reify-pull-requests-by-making-them-a-ref + e.printStackTrace(request.listener().getLogger()); + if (e.getHttpCode() == 403) { + // the credentials do not have permission, so we should not observe the + // PR ever the PR is dead to us, so this is the one case where we can + // squash the exception. + throw new Skip(); + } + throw e; + } + } + }, + new BitbucketProbeFactory(pullBitbucket, request), + new BitbucketRevisionFactory() { + @NonNull + @Override + public SCMRevision create(@NonNull SCMHead head, @Nullable String hash) + throws IOException, InterruptedException { + if (head instanceof PullRequestSCMHead) { + PullRequestSCMHead h = (PullRequestSCMHead) head; + for (BitbucketBranch b : request.getBranches()) { + if (b.getName().equals(h.getTarget().getName())) { + if (repositoryType == BitbucketRepositoryType.MERCURIAL) { + return new PullRequestSCMRevision<>( + h, + new MercurialRevision(h.getTarget(), b.getRawNode()), + new MercurialRevision(h, hash) + ); + } else { + return new PullRequestSCMRevision<>(h, + new SCMRevisionImpl( + h.getTarget(), + b.getRawNode() + ), + new SCMRevisionImpl( + h, + hash + ) + ); + } + } + } + } + return super.create(head, hash); + } + }, new CriteriaWitness(request))) { + request.listener().getLogger() + .format("%n %d pull requests were processed (query completed)%n", count); + return; } } - getPullRequestTitleCache().put(pull.getId(), StringUtils.defaultString(pull.getTitle())); - livePRs.add(pull.getId()); - getPullRequestContributorCache().put(pull.getId(), - // TODO get more details on the author - new ContributorMetadataAction(pull.getAuthorLogin(), null, null) - ); - observe(criteria, observer, listener, - pull.getSource().getRepository().getOwnerName(), - pull.getSource().getRepository().getRepositoryName(), - pull.getSource().getBranch().getName(), - hash, - head); - if (!observer.isObserving()) { - return; - } + } catch (Skip e) { + request.listener().getLogger().println( + "Do not have permission to view PR from " + pull.getSource().getRepository() + .getFullName() + + " and branch " + + pull.getSource().getBranch().getName()); + continue; + } catch (Throwable t) { + // TODO remove + t.printStackTrace(request.listener().getLogger()); } - getPullRequestTitleCache().keySet().retainAll(livePRs); - getPullRequestContributorCache().keySet().retainAll(livePRs); - } else { - listener.getLogger().format("Skipping pull requests for public repositories%n"); } + request.listener().getLogger().format("%n %d pull requests were processed%n", count); + getPullRequestTitleCache().keySet().retainAll(livePRs); + getPullRequestContributorCache().keySet().retainAll(livePRs); } - private void retrieveBranches(SCMSourceCriteria criteria, @NonNull final SCMHeadObserver observer, - @NonNull TaskListener listener) + private void retrieveBranches(final BitbucketSCMSourceRequest request) throws IOException, InterruptedException { String fullName = repoOwner + "/" + repository; - listener.getLogger().println("Looking up " + fullName + " for branches"); + 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"); } - List branches = bitbucket.getBranches(); - Set includes = observer.getIncludes(); - for (BitbucketBranch branch : branches) { - checkInterrupt(); - BranchSCMHead head = new BranchSCMHead(branch.getName(), repositoryType); - if (includes != null && !includes.contains(head)) { - continue; - } - listener.getLogger().println("Checking branch " + branch.getName() + " from " + fullName); - observe(criteria, observer, listener, repoOwner, repository, branch.getName(), - branch.getRawNode(), head); - if (!observer.isObserving()) { + int count = 0; + for (final BitbucketBranch branch : request.getBranches()) { + request.listener().getLogger().println("Checking branch " + branch.getName() + " from " + fullName); + count++; + if (request.process(new BranchSCMHead(branch.getName(), repositoryType), + new SCMSourceRequest.IntermediateLambda() { + @Nullable + @Override + public String create() { + return branch.getRawNode(); + } + }, new BitbucketProbeFactory(bitbucket, request), new BitbucketRevisionFactory(), + new CriteriaWitness(request) + )) { + request.listener().getLogger().format("%n %d branches were processed (query completed)%n", count); return; } } + request.listener().getLogger().format("%n %d branches were processed%n", count); } - private void observe(SCMSourceCriteria criteria, SCMHeadObserver observer, final TaskListener listener, - final String owner, final String repositoryName, - final String branchName, final String hash, SCMHead head) throws IOException, InterruptedException { - if (isExcluded(branchName)) { - return; - } - final BitbucketApi bitbucket = BitbucketApiFactory.newInstance(bitbucketServerUrl, getScanCredentials(), owner, repositoryName); - - if (criteria != null) { - SCMSourceCriteria.Probe probe = new SCMSourceCriteria.Probe() { - - @Override - public String name() { - return branchName; - } - - @Override - public long lastModified() { - try { - BitbucketCommit commit = bitbucket.resolveCommit(hash); - if (commit == null) { - listener.getLogger().format("Can not resolve commit by hash [%s] on repository %s/%s%n", - hash, bitbucket.getOwner(), bitbucket.getRepositoryName()); - return 0; - } - return commit.getDateMillis(); - } catch (InterruptedException | IOException e) { - listener.getLogger().format("Can not resolve commit by hash [%s] on repository %s/%s%n", - hash, bitbucket.getOwner(), bitbucket.getRepositoryName()); - return 0; - } - } - - @Override - public boolean exists(@NonNull String path) throws IOException { - try { - // TODO should be checking the revision not the head - return bitbucket.checkPathExists(branchName, path); - } catch (InterruptedException e) { - throw new IOException("Interrupted", e); - } - } - }; - if (criteria.isHead(probe, listener)) { - listener.getLogger().println("Met criteria"); + @Override + protected SCMRevision retrieve(SCMHead head, TaskListener listener) throws IOException, InterruptedException { + List branches = buildBitbucketClient().getBranches(); + if (head instanceof PullRequestSCMHead) { + PullRequestSCMHead h = (PullRequestSCMHead) head; + String targetRevision = findRawNode(h.getTarget().getName(), branches, listener); + if (targetRevision == null) { + LOGGER.log(Level.WARNING, "No branch found in {0}/{1} with name [{2}]", + new Object[]{repoOwner, repository, h.getTarget().getName()}); + return null; + } + branches = head.getOrigin() == SCMHeadOrigin.DEFAULT + ? branches + : buildBitbucketClient(h).getBranches(); + String sourceRevision = findRawNode(h.getBranchName(), branches, listener); + if (sourceRevision == null) { + LOGGER.log(Level.WARNING, "No branch found in {0}/{1} with name [{2}]", + new Object[]{ + h.getRepoOwner(), + h.getRepository(), + h.getBranchName() + }); + return null; + } + if (getRepositoryType() == BitbucketRepositoryType.MERCURIAL) { + return new PullRequestSCMRevision<>( + h, + new MercurialRevision(h.getTarget(), targetRevision), + new MercurialRevision(h, sourceRevision) + ); } else { - listener.getLogger().println("Does not meet criteria"); - return; + return new PullRequestSCMRevision<>( + h, + new SCMRevisionImpl(h.getTarget(), targetRevision), + new SCMRevisionImpl(h, sourceRevision) + ); } - } - BitbucketRepositoryType repositoryType = getRepositoryType(); - SCMRevision revision; - if (repositoryType == BitbucketRepositoryType.MERCURIAL) { - revision = new MercurialRevision(head, hash); } else { - revision = new AbstractGitSCMSource.SCMRevisionImpl(head, hash); + String revision = findRawNode(head.getName(), branches, listener); + if (revision == null) { + LOGGER.log(Level.WARNING, "No branch found in {0}/{1} with name [{2}]", + new Object[]{repoOwner, repository, head.getName()}); + return null; + } + if (getRepositoryType() == BitbucketRepositoryType.MERCURIAL) { + return new MercurialRevision(head, revision); + } else { + return new SCMRevisionImpl(head, revision); + } } - observer.observe(head, revision); } - - - @Override - protected SCMRevision retrieve(SCMHead head, TaskListener listener) throws IOException, InterruptedException { - BitbucketApi bitbucket = head instanceof PullRequestSCMHead - ? buildBitbucketClient((PullRequestSCMHead) head) - : buildBitbucketClient(); - String branchName = head instanceof PullRequestSCMHead ? ((PullRequestSCMHead) head).getBranchName() : head.getName(); - List branches = bitbucket.getBranches(); + private String findRawNode(String branchName, List branches, TaskListener listener) { for (BitbucketBranch b : branches) { if (branchName.equals(b.getName())) { - if (b.getRawNode() == null) { - if (getBitbucketServerUrl() == null) { - listener.getLogger().format("Cannot resolve the hash of the revision in branch %s", b.getName()); + String revision = b.getRawNode(); + if (revision == null) { + if (BitbucketCloudEndpoint.SERVER_URL.equals(getServerUrl())) { + listener.getLogger().format("Cannot resolve the hash of the revision in branch %s%n", + branchName); } else { - listener.getLogger().format("Cannot resolve the hash of the revision in branch %s. Perhaps you are using Bitbucket Server previous to 4.x", b.getName()); + listener.getLogger().format("Cannot resolve the hash of the revision in branch %s. " + + "Perhaps you are using Bitbucket Server previous to 4.x%n", + branchName); } return null; } - if (getRepositoryType() == BitbucketRepositoryType.MERCURIAL) { - return new MercurialRevision(head, b.getRawNode()); - } else { - return new AbstractGitSCMSource.SCMRevisionImpl(head, b.getRawNode()); - } + return revision; } } - LOGGER.log(Level.WARNING, "No branch found in {0}/{1} with name [{2}]", head instanceof PullRequestSCMHead - ? new Object[]{ - ((PullRequestSCMHead) head).getRepoOwner(), - ((PullRequestSCMHead) head).getRepository(), - ((PullRequestSCMHead) head).getBranchName()} - : new Object[]{repoOwner, repository, head.getName()}); + listener.getLogger().format("Cannot find the branch %s%n", branchName); return null; } @Override public SCM build(SCMHead head, SCMRevision revision) { - BitbucketRepositoryType repositoryType; + BitbucketRepositoryType type; if (head instanceof PullRequestSCMHead) { - repositoryType = ((PullRequestSCMHead) head).getRepositoryType(); + type = ((PullRequestSCMHead) head).getRepositoryType(); } else if (head instanceof BranchSCMHead) { - repositoryType = ((BranchSCMHead) head).getRepositoryType(); + type = ((BranchSCMHead) head).getRepositoryType(); } else { throw new IllegalArgumentException("Either PullRequestSCMHead or BranchSCMHead required as parameter"); } - if (repositoryType == null) { + if (type == null) { if (revision instanceof MercurialRevision) { - repositoryType = BitbucketRepositoryType.MERCURIAL; - } else if (revision instanceof AbstractGitSCMSource.SCMRevisionImpl) { - repositoryType = BitbucketRepositoryType.GIT; + type = BitbucketRepositoryType.MERCURIAL; + } else if (revision instanceof SCMRevisionImpl) { + type = BitbucketRepositoryType.GIT; } else { try { - repositoryType = getRepositoryType(); + type = getRepositoryType(); } catch (IOException | InterruptedException e) { - repositoryType = BitbucketRepositoryType.GIT; + type = BitbucketRepositoryType.GIT; LOGGER.log(Level.SEVERE, "Could not determine repository type of " + getRepoOwner() + "/" + getRepository() - + " on " + StringUtils.defaultIfBlank(getBitbucketServerUrl(), "bitbucket.org") - + " for " + getOwner() + " assuming " + repositoryType, e); + + " on " + getServerUrl() + " for " + getOwner() + " assuming " + type, e); } } } + assert type != null; if (cloneLinks == null) { BitbucketApi bitbucket = buildBitbucketClient(); try { @@ -568,12 +862,12 @@ public SCM build(SCMHead head, SCMRevision revision) { } catch (IOException | InterruptedException e) { LOGGER.log(Level.SEVERE, "Could not determine clone links of " + getRepoOwner() + "/" + getRepository() - + " on " + StringUtils.defaultIfBlank(getBitbucketServerUrl(), "bitbucket.org") - + " for " + getOwner() + " falling back to generated links", e); + + " on " + getServerUrl() + " for " + getOwner() + " falling back to generated links", + e); cloneLinks = new ArrayList<>(); cloneLinks.add(new BitbucketHref("ssh", bitbucket.getRepositoryUri( - repositoryType, + type, BitbucketRepositoryProtocol.SSH, null, getRepoOwner(), @@ -582,7 +876,7 @@ public SCM build(SCMHead head, SCMRevision revision) { )); cloneLinks.add(new BitbucketHref("https", bitbucket.getRepositoryUri( - repositoryType, + type, BitbucketRepositoryProtocol.HTTP, null, getRepoOwner(), @@ -591,65 +885,44 @@ public SCM build(SCMHead head, SCMRevision revision) { )); } } - if (head instanceof PullRequestSCMHead) { - PullRequestSCMHead h = (PullRequestSCMHead) head; - if (repositoryType == BitbucketRepositoryType.MERCURIAL) { - MercurialSCM scm = new MercurialSCM(getRemote(h.getRepoOwner(), h.getRepository(), - BitbucketRepositoryType.MERCURIAL)); - // If no revision specified the branch name will be used as revision - scm.setRevision(revision instanceof MercurialRevision - ? ((MercurialRevision) revision).getHash() - : h.getBranchName() - ); - scm.setRevisionType(RevisionType.BRANCH); - scm.setCredentialsId(getCheckoutEffectiveCredentials()); - return scm; - } else { - // Defaults to Git - BuildChooser buildChooser = revision instanceof AbstractGitSCMSource.SCMRevisionImpl - ? new SpecificRevisionBuildChooser((AbstractGitSCMSource.SCMRevisionImpl) revision) - : new DefaultBuildChooser(); - return new GitSCM(getGitRemoteConfigs(h), - Collections.singletonList(new BranchSpec(h.getBranchName())), - false, Collections.emptyList(), - null, null, Collections.singletonList(new BuildChooserSetting(buildChooser))); - } - } - // head instanceof BranchSCMHead - if (repositoryType == BitbucketRepositoryType.MERCURIAL) { - MercurialSCM scm = new MercurialSCM(getRemote(repoOwner, repository, BitbucketRepositoryType.MERCURIAL)); - // If no revision specified the branch name will be used as revision - scm.setRevision(revision instanceof MercurialRevision - ? ((MercurialRevision) revision).getHash() - : head.getName() - ); - scm.setRevisionType(RevisionType.BRANCH); - scm.setCredentialsId(getCheckoutEffectiveCredentials()); - return scm; - } else { - // Defaults to Git - BuildChooser buildChooser = revision instanceof AbstractGitSCMSource.SCMRevisionImpl - ? new SpecificRevisionBuildChooser((AbstractGitSCMSource.SCMRevisionImpl) revision) - : new DefaultBuildChooser(); - return new GitSCM(getGitRemoteConfigs((BranchSCMHead)head), - Collections.singletonList(new BranchSpec(head.getName())), - false, Collections.emptyList(), - null, null, Collections.singletonList(new BuildChooserSetting(buildChooser))); - } - } + switch (type) { + case MERCURIAL: + return new BitbucketHgSCMBuilder(this, head, revision, getCredentialsId()) + .withCloneLinks(cloneLinks) + .withTraits(traits) + .build(); + case GIT: + default: + return new BitbucketGitSCMBuilder(this, head, revision, getCredentialsId()) + .withCloneLinks(cloneLinks) + .withTraits(traits) + .build(); - protected List getGitRemoteConfigs(BranchSCMHead head) { - List result = new ArrayList(); - String remote = getRemote(repoOwner, repository, BitbucketRepositoryType.GIT); - result.add(new UserRemoteConfig(remote, getRemoteName(), "+refs/heads/" + head.getName(), getCheckoutEffectiveCredentials())); - return result; + } } - protected List getGitRemoteConfigs(PullRequestSCMHead head) { - List result = new ArrayList(); - String remote = getRemote(head.getRepoOwner(), head.getRepository(), BitbucketRepositoryType.GIT); - result.add(new UserRemoteConfig(remote, getRemoteName(), "+refs/heads/" + head.getBranchName(), getCheckoutEffectiveCredentials())); - return result; + @NonNull + @Override + public SCMRevision getTrustedRevision(@NonNull SCMRevision revision, @NonNull TaskListener listener) + throws IOException, InterruptedException { + if (revision instanceof PullRequestSCMRevision) { + PullRequestSCMHead head = (PullRequestSCMHead) revision.getHead(); + + try (BitbucketSCMSourceRequest request = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()) + .withTraits(traits) + .newRequest(this, listener)) { + if (request.isTrusted(head)) { + return revision; + } + } catch (WrappedException wrapped) { + wrapped.unwrap(); + } + 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(); + } + return revision; } @Override @@ -658,77 +931,15 @@ public DescriptorImpl getDescriptor() { } @CheckForNull - /* package */ StandardUsernamePasswordCredentials getScanCredentials() { + /* package */ StandardUsernamePasswordCredentials credentials() { return BitbucketCredentials.lookupCredentials( - bitbucketServerUrl, + getServerUrl(), getOwner(), - credentialsId, + getCredentialsId(), StandardUsernamePasswordCredentials.class ); } - private StandardCredentials getCheckoutCredentials() { - return BitbucketCredentials.lookupCredentials( - bitbucketServerUrl, - getOwner(), - getCheckoutEffectiveCredentials(), - StandardCredentials.class - ); - } - - public String getRemoteName() { - return "origin"; - } - - /** - * Returns true if the branchName isn't matched by includes or is matched by excludes. - * - * @param branchName - * @return true if branchName is excluded or is not included - */ - private boolean isExcluded(String branchName) { - return !Pattern.matches(getPattern(getIncludes()), branchName) - || Pattern.matches(getPattern(getExcludes()), branchName); - } - - /** - * Returns the pattern corresponding to the branches containing wildcards. - * - * @param branches space separated list of expressions. - * For example "*" which would match all branches and branch* would match branch1, branch2, etc. - * @return pattern corresponding to the branches containing wildcards (ready to be used by {@link Pattern}) - */ - private String getPattern(String branches) { - StringBuilder quotedBranches = new StringBuilder(); - for (String wildcard : branches.split(" ")) { - StringBuilder quotedBranch = new StringBuilder(); - for (String branch : wildcard.split("\\*")) { - if (wildcard.startsWith("*") || quotedBranches.length() > 0) { - quotedBranch.append(".*"); - } - quotedBranch.append(Pattern.quote(branch)); - } - if (wildcard.endsWith("*")) { - quotedBranch.append(".*"); - } - if (quotedBranches.length() > 0) { - quotedBranches.append("|"); - } - quotedBranches.append(quotedBranch); - } - return quotedBranches.toString(); - } - - private String getCheckoutEffectiveCredentials() { - if (DescriptorImpl.ANONYMOUS.equals(checkoutCredentialsId)) { - return null; - } else if (DescriptorImpl.SAME.equals(checkoutCredentialsId)) { - return credentialsId; - } else { - return checkoutCredentialsId; - } - } - @NonNull @Override protected List retrieveActions(@CheckForNull SCMSourceEvent event, @@ -747,16 +958,16 @@ protected List retrieveActions(@CheckForNull SCMSourceEvent event, if (StringUtils.isNotBlank(defaultBranch)) { result.add(new BitbucketDefaultBranch(repoOwner, repository, defaultBranch)); } - String serverUrl = StringUtils.removeEnd(bitbucketUrl(), "/"); - if (StringUtils.isNotEmpty(bitbucketServerUrl)) { + if (BitbucketCloudEndpoint.SERVER_URL.equals(getServerUrl())) { result.add(new BitbucketLink("icon-bitbucket-repo", - serverUrl + "/projects/" + repoOwner + "/repos/" + repository)); - result.add(new ObjectMetadataAction(r.getFullName(), null, - serverUrl + "/projects/" + repoOwner + "/repos/" + repository)); + getServerUrl() + "/" + repoOwner + "/" + repository)); + result.add(new ObjectMetadataAction(r.getRepositoryName(), null, + getServerUrl() + "/" + repoOwner + "/" + repository)); } else { - result.add(new BitbucketLink("icon-bitbucket-repo", serverUrl + "/" + repoOwner + "/" + repository)); - result.add(new ObjectMetadataAction(r.getFullName(), null, - serverUrl + "/" + repoOwner + "/" + repository)); + result.add(new BitbucketLink("icon-bitbucket-repo", + getServerUrl() + "/projects/" + repoOwner + "/repos/" + repository)); + result.add(new ObjectMetadataAction(r.getRepositoryName(), null, + getServerUrl() + "/projects/" + repoOwner + "/repos/" + repository)); } return result; } @@ -769,42 +980,41 @@ protected List retrieveActions(@NonNull SCMHead head, throws IOException, InterruptedException { // TODO when we have support for trusted events, use the details from event if event was from trusted source List result = new ArrayList<>(); - String serverUrl = StringUtils.removeEnd(bitbucketUrl(), "/"); - if (StringUtils.isNotEmpty(bitbucketServerUrl)) { + if (BitbucketCloudEndpoint.SERVER_URL.equals(getServerUrl())) { String branchUrl; String title; if (head instanceof PullRequestSCMHead) { PullRequestSCMHead pr = (PullRequestSCMHead) head; - branchUrl = "projects/" + repoOwner + "/repos/" + repository + "/pull-requests/"+pr.getId()+"/overview"; + branchUrl = repoOwner + "/" + repository + "/pull-requests/" + pr.getId(); title = getPullRequestTitleCache().get(pr.getId()); ContributorMetadataAction contributor = getPullRequestContributorCache().get(pr.getId()); if (contributor != null) { result.add(contributor); } } else { - branchUrl = "projects/" + repoOwner + "/repos/" + repository + "/compare/commits?sourceBranch=" + - URLEncoder.encode(Constants.R_HEADS + head.getName(), "UTF-8"); + branchUrl = repoOwner + "/" + repository + "/branch/" + Util.rawEncode(head.getName()); title = null; } - result.add(new BitbucketLink("icon-bitbucket-branch", serverUrl + "/" + branchUrl)); - result.add(new ObjectMetadataAction(title, null, serverUrl+"/"+branchUrl)); + result.add(new BitbucketLink("icon-bitbucket-branch", getServerUrl() + "/" + branchUrl)); + result.add(new ObjectMetadataAction(title, null, getServerUrl() + "/" + branchUrl)); } else { String branchUrl; String title; if (head instanceof PullRequestSCMHead) { PullRequestSCMHead pr = (PullRequestSCMHead) head; - branchUrl = repoOwner + "/" + repository + "/pull-requests/" + pr.getId(); + branchUrl = "projects/" + repoOwner + "/repos/" + repository + "/pull-requests/" +pr.getId()+"/overview"; title = getPullRequestTitleCache().get(pr.getId()); ContributorMetadataAction contributor = getPullRequestContributorCache().get(pr.getId()); if (contributor != null) { result.add(contributor); } } else { - branchUrl = repoOwner + "/" + repository + "/branch/" + head.getName(); + branchUrl = "projects/" + repoOwner + "/repos/" + repository + "/compare/commits" + + "?sourceBranch=" + URLEncoder.encode(Constants.R_HEADS + head.getName(), "UTF-8"); title = null; } - result.add(new BitbucketLink("icon-bitbucket-branch", serverUrl + "/" + branchUrl)); - result.add(new ObjectMetadataAction(title, null, serverUrl + "/" + branchUrl)); + result.add(new BitbucketLink("icon-bitbucket-branch", getServerUrl() + "/" + branchUrl)); + result.add(new ObjectMetadataAction(title, null, getServerUrl()+"/"+branchUrl)); } SCMSourceOwner owner = getOwner(); if (owner instanceof Actionable) { @@ -836,6 +1046,17 @@ private synchronized Map getPullRequestContri return pullRequestContributorCache; } + @NonNull + public SCMHeadOrigin originOf(@NonNull String repoOwner, @NonNull String repository) { + if (this.repository.equalsIgnoreCase(repository)) { + if (this.repoOwner.equalsIgnoreCase(repoOwner)) { + return SCMHeadOrigin.DEFAULT; + } + return new SCMHeadOrigin.Fork(repoOwner); + } + return new SCMHeadOrigin.Fork(repoOwner + "/" + repository); + } + @Symbol("bitbucket") @Extension public static class DescriptorImpl extends SCMSourceDescriptor { @@ -857,6 +1078,8 @@ public FormValidation doCheckCredentialsId(@QueryParameter String value, } } + @Restricted(NoExternalUse.class) + @Deprecated public static FormValidation doCheckBitbucketServerUrl(@QueryParameter String bitbucketServerUrl) { String url = Util.fixEmpty(bitbucketServerUrl); if (url == null) { @@ -870,17 +1093,106 @@ public static FormValidation doCheckBitbucketServerUrl(@QueryParameter String bi return FormValidation.ok(); } - public ListBoxModel doFillCredentialsIdItems(@AncestorInPath SCMSourceOwner context, @QueryParameter String bitbucketServerUrl) { + public static FormValidation doCheckServerUrl(@QueryParameter String value) { + if (BitbucketEndpointConfiguration.get().findEndpoint(value) == null) { + return FormValidation.error("Unregistered Server: " + value); + } + return FormValidation.ok(); + } + + public boolean isServerUrlSelectable() { + return BitbucketEndpointConfiguration.get().isEndpointSelectable(); + } + + public ListBoxModel doFillServerUrlItems() { + return BitbucketEndpointConfiguration.get().getEndpointItems(); + } + + public ListBoxModel doFillCredentialsIdItems(@AncestorInPath SCMSourceOwner context, @QueryParameter String serverUrl) { StandardListBoxModel result = new StandardListBoxModel(); result.includeEmptyValue(); - return BitbucketCredentials.fillCredentials(bitbucketServerUrl, context, result); + result.includeMatchingAs( + context instanceof Queue.Task + ? Tasks.getDefaultAuthenticationOf((Queue.Task) context) + : ACL.SYSTEM, + context, + StandardUsernameCredentials.class, + URIRequirementBuilder.fromUri(serverUrl).build(), + CredentialsMatchers.anyOf(CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class)) + ); + return result; } + public ListBoxModel doFillRepositoryItems(@AncestorInPath SCMSourceOwner context, + @QueryParameter String serverUrl, + @QueryParameter String credentialsId, + @QueryParameter String repoOwner) + throws IOException, InterruptedException { + if (StringUtils.isBlank(repoOwner)) { + return new ListBoxModel(); + } + context.getACL().checkPermission(Item.CONFIGURE); + serverUrl = StringUtils.defaultIfBlank(serverUrl, BitbucketCloudEndpoint.SERVER_URL); + ListBoxModel result = new ListBoxModel(); + StandardUsernamePasswordCredentials credentials = BitbucketCredentials.lookupCredentials( + serverUrl, + context, + credentialsId, + StandardUsernamePasswordCredentials.class + ); + try { + BitbucketApi bitbucket = BitbucketApiFactory.newInstance(serverUrl, credentials, repoOwner, null); + BitbucketTeam team = bitbucket.getTeam(); + List repositories = + bitbucket.getRepositories(team != null ? null : UserRoleInRepository.OWNER); + if (repositories.isEmpty()) { + throw new FillErrorResponse(Messages.BitbucketSCMSource_NoMatchingOwner(repoOwner), true); + } + for (BitbucketRepository repo : repositories) { + result.add(repo.getRepositoryName()); + } + return result; + } catch (FillErrorResponse | OutOfMemoryError e) { + throw e; + } catch (IOException e) { + if (e instanceof BitbucketRequestException) { + if (((BitbucketRequestException) e).getHttpCode() == 401) { + throw new FillErrorResponse(credentials == null + ? Messages.BitbucketSCMSource_UnauthorizedAnonymous(repoOwner) + : Messages.BitbucketSCMSource_UnauthorizedOwner(repoOwner), true); + } + } else if (e.getCause() instanceof BitbucketRequestException) { + if (((BitbucketRequestException) e.getCause()).getHttpCode() == 401) { + throw new FillErrorResponse(credentials == null + ? Messages.BitbucketSCMSource_UnauthorizedAnonymous(repoOwner) + : Messages.BitbucketSCMSource_UnauthorizedOwner(repoOwner), true); + } + } + LOGGER.log(Level.SEVERE, e.getMessage(), e); + throw new FillErrorResponse(e.getMessage(), false); + } catch (Throwable e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + throw new FillErrorResponse(e.getMessage(), false); + } + } + + @Deprecated + @Restricted(DoNotUse.class) + @RestrictedSince("2.2.0") public ListBoxModel doFillCheckoutCredentialsIdItems(@AncestorInPath SCMSourceOwner context, @QueryParameter String bitbucketServerUrl) { StandardListBoxModel result = new StandardListBoxModel(); result.add("- same as scan credentials -", SAME); result.add("- anonymous -", ANONYMOUS); - return BitbucketCredentials.fillCheckoutCredentials(bitbucketServerUrl, context, result); + result.includeMatchingAs( + context instanceof Queue.Task + ? Tasks.getDefaultAuthenticationOf((Queue.Task) context) + : ACL.SYSTEM, + context, + StandardCredentials.class, + URIRequirementBuilder.fromUri(bitbucketServerUrl).build(), + CredentialsMatchers.anyOf(CredentialsMatchers.instanceOf(StandardCredentials.class)) + ); + return result; } @NonNull @@ -892,6 +1204,54 @@ protected SCMHeadCategory[] createCategories() { // TODO add support for tags and maybe feature branch identification }; } + + public List> getTraitsDescriptorLists() { + List all = + SCMSourceTrait._for(this, BitbucketSCMSourceContext.class, null); + all.addAll(SCMSourceTrait._for(this, null, BitbucketGitSCMBuilder.class)); + all.addAll(SCMSourceTrait._for(this, null, BitbucketHgSCMBuilder.class)); + Set dedup = new HashSet<>(); + for (Iterator iterator = all.iterator(); iterator.hasNext(); ) { + SCMSourceTraitDescriptor d = iterator.next(); + if (dedup.contains(d) + || d instanceof MercurialBrowserSCMSourceTrait.DescriptorImpl + || d instanceof GitBrowserSCMSourceTrait.DescriptorImpl) { + // remove any we have seen already and ban the browser configuration as it will always be bitbucket + iterator.remove(); + } else { + dedup.add(d); + } + } + List> result = new ArrayList<>(); + NamedArrayList.select(all, "Within repository", NamedArrayList + .anyOf(NamedArrayList.withAnnotation(Discovery.class), + NamedArrayList.withAnnotation(Selection.class)), + true, result); + int insertionPoint = result.size(); + NamedArrayList.select(all, "Git", new NamedArrayList.Predicate() { + @Override + public boolean test(SCMSourceTraitDescriptor d) { + return GitSCM.class.isAssignableFrom(d.getScmClass()); + } + }, true, result); + NamedArrayList.select(all, "Mercurial", new NamedArrayList.Predicate() { + @Override + public boolean test(SCMSourceTraitDescriptor d) { + return MercurialSCM.class.isAssignableFrom(d.getScmClass()); + } + }, true, result); + NamedArrayList.select(all, "General", null, true, result, insertionPoint); + return result; + } + + public List getTraitsDefaults() { + return Arrays.asList( + new BranchDiscoveryTrait(true, false), + new OriginPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.MERGE)), + new ForkPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.MERGE), + new ForkPullRequestDiscoveryTrait.TrustTeamForks()) + ); + } } public static class MercurialRevision extends SCMRevision { @@ -936,4 +1296,113 @@ public String toString() { } + private static class CriteriaWitness implements SCMSourceRequest.Witness { + private final BitbucketSCMSourceRequest request; + + public CriteriaWitness(BitbucketSCMSourceRequest request) { + this.request = request; + } + + @Override + public void record(@NonNull SCMHead scmHead, SCMRevision revision, boolean isMatch) { + if (revision == null) { + request.listener().getLogger().println(" Skipped"); + } else { + if (isMatch) { + request.listener().getLogger().println(" Met criteria"); + } else { + request.listener().getLogger().println(" Does not meet criteria"); + return; + } + + } + } + } + + private static class BitbucketProbeFactory implements SCMSourceRequest.ProbeLambda { + private final BitbucketApi bitbucket; + private final BitbucketSCMSourceRequest request; + + public BitbucketProbeFactory(BitbucketApi bitbucket, BitbucketSCMSourceRequest request) { + this.bitbucket = bitbucket; + this.request = request; + } + + @NonNull + @Override + public SCMSourceCriteria.Probe create(@NonNull final SCMHead head, @Nullable final String hash) + throws IOException, InterruptedException { + return new SCMSourceCriteria.Probe() { + @Override + public String name() { + return head.getName(); + } + + @Override + public long lastModified() { + try { + BitbucketCommit commit = bitbucket.resolveCommit(hash); + if (commit == null) { + request.listener().getLogger() + .format("Can not resolve commit by hash [%s] on repository %s/%s%n", + hash, bitbucket.getOwner(), bitbucket.getRepositoryName()); + return 0; + } + return commit.getDateMillis(); + } catch (InterruptedException | IOException e) { + request.listener().getLogger() + .format("Can not resolve commit by hash [%s] on repository %s/%s%n", + hash, bitbucket.getOwner(), bitbucket.getRepositoryName()); + return 0; + } + } + + @Override + public boolean exists(@NonNull String path) throws IOException { + try { + return bitbucket.checkPathExists(hash, path); + } catch (InterruptedException e) { + throw new IOException("Interrupted", e); + } + } + }; + } + } + + private class BitbucketRevisionFactory + implements SCMSourceRequest.LazyRevisionLambda { + @NonNull + @Override + public SCMRevision create(@NonNull SCMHead head, @Nullable String hash) + throws IOException, InterruptedException { + if (repositoryType == BitbucketRepositoryType.MERCURIAL) { + return new MercurialRevision(head, hash); + } else { + return new SCMRevisionImpl(head, hash); + } + } + } + + private static class WrappedException extends RuntimeException { + + public WrappedException(Throwable cause) { + super(cause); + } + + public void unwrap() throws IOException, InterruptedException { + Throwable cause = getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } + if (cause instanceof InterruptedException) { + throw (InterruptedException) cause; + } + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + throw this; + } + + } + } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceBuilder.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceBuilder.java new file mode 100644 index 000000000..26c511e7f --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceBuilder.java @@ -0,0 +1,125 @@ +/* + * The MIT License + * + * Copyright (c) 2017, 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.endpoints.BitbucketEndpointConfiguration; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import jenkins.scm.api.trait.SCMSourceBuilder; + +/** + * A {@link SCMSourceBuilder} that builds {@link BitbucketSCMSource} instances + * + * @since 2.2.0 + */ +public class BitbucketSCMSourceBuilder extends SCMSourceBuilder { + /** + * The {@link BitbucketSCMSource#getId()}. + */ + @CheckForNull + private final String id; + /** + * The {@link BitbucketSCMSource#getServerUrl()} + */ + @NonNull + private final String serverUrl; + /** + * The credentials id or {@code null} to use anonymous scanning. + */ + @CheckForNull + private final String credentialsId; + /** + * The repository owner. + */ + @NonNull + private final String repoOwner; + + /** + * Constructor. + * + * @param id the {@link BitbucketSCMSource#getId()} + * @param serverUrl the {@link BitbucketSCMSource#getBitbucketServerUrl()}; + * @param credentialsId the credentials id. + * @param repoOwner the repository owner. + * @param repoName the project name. + */ + public BitbucketSCMSourceBuilder(@CheckForNull String id, @NonNull String serverUrl, + @CheckForNull String credentialsId, @NonNull String repoOwner, + @NonNull String repoName) { + super(BitbucketSCMSource.class, repoName); + this.id = id; + this.serverUrl = BitbucketEndpointConfiguration.normalizeServerUrl(serverUrl); + this.credentialsId = credentialsId; + this.repoOwner = repoOwner; + } + + /** + * The id of the {@link BitbucketSCMSource} that is being built. + * + * @return the id of the {@link BitbucketSCMSource} that is being built. + */ + public final String id() { + return id; + } + + /** + * The server url of the {@link BitbucketSCMSource} that is being built. + * + * @return the server url of the {@link BitbucketSCMSource} that is being built. + */ + public final String serverUrl() { + return serverUrl; + } + + /** + * The credentials that the {@link BitbucketSCMSource} will use. + * + * @return the credentials that the {@link BitbucketSCMSource} will use. + */ + public final String credentialsId() { + return credentialsId; + } + + /** + * The repository owner that the {@link BitbucketSCMSource} will be configured to use. + * + * @return the repository owner that the {@link BitbucketSCMSource} will be configured to use. + */ + public final String repoOwner() { + return repoOwner; + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public BitbucketSCMSource build() { + BitbucketSCMSource result = new BitbucketSCMSource(id(), repoOwner(), projectName()); + result.setServerUrl(serverUrl()); + result.setCredentialsId(credentialsId()); + result.setTraits(traits()); + return result; + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceContext.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceContext.java index 6ca662462..70266a400 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceContext.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceContext.java @@ -56,6 +56,10 @@ public class BitbucketSCMSourceContext extends SCMSourceContextemptySet(); @@ -210,6 +215,15 @@ public final boolean isFetchForkPRs() { return fetchForkPRs; } + /** + * Returns {@code true} if pull requests from public repositories should be skipped. + * + * @return {@code true} if pull requests from public repositories should be skipped. + */ + public final boolean isSkipPublicPRs() { + return skipPublicPRs; + } + /** * Returns the {@link ChangeRequestCheckoutStrategy} to create for each origin pull request. * diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait.java new file mode 100644 index 000000000..980798fd1 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait.java @@ -0,0 +1,265 @@ +/* + * The MIT License + * + * Copyright (c) 2017, 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.BitbucketPullRequest; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.util.ListBoxModel; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMHeadCategory; +import jenkins.scm.api.SCMHeadOrigin; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.trait.SCMHeadAuthority; +import jenkins.scm.api.trait.SCMHeadAuthorityDescriptor; +import jenkins.scm.api.trait.SCMHeadFilter; +import jenkins.scm.api.trait.SCMSourceContext; +import jenkins.scm.api.trait.SCMSourceRequest; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMSourceTraitDescriptor; +import jenkins.scm.impl.trait.Discovery; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * A {@link Discovery} trait for bitbucket that will discover branches on the repository. + * + * @since 2.2.0 + */ +public class BranchDiscoveryTrait extends SCMSourceTrait { + /** + * The strategy encoded as a bit-field. + */ + private int strategyId; + + /** + * Constructor for stapler. + * + * @param strategyId the strategy id. + */ + @DataBoundConstructor + public BranchDiscoveryTrait(int strategyId) { + this.strategyId = strategyId; + } + + /** + * Constructor for legacy code. + * + * @param buildBranch build branches that are not filed as a PR. + * @param buildBranchWithPr build branches that are also PRs. + */ + public BranchDiscoveryTrait(boolean buildBranch, boolean buildBranchWithPr) { + this.strategyId = (buildBranch ? 1 : 0) + (buildBranchWithPr ? 2 : 0); + } + + /** + * Returns the strategy id. + * + * @return the strategy id. + */ + public int getStrategyId() { + return strategyId; + } + + /** + * Returns {@code true} if building branches that are not filed as a PR. + * + * @return {@code true} if building branches that are not filed as a PR. + */ + @Restricted(NoExternalUse.class) + public boolean isBuildBranch() { + return (strategyId & 1) != 0; + + } + + /** + * Returns {@code true} if building branches that are filed as a PR. + * + * @return {@code true} if building branches that are filed as a PR. + */ + @Restricted(NoExternalUse.class) + public boolean isBuildBranchesWithPR() { + return (strategyId & 2) != 0; + } + + /** + * {@inheritDoc} + */ + @Override + protected void decorateContext(SCMSourceContext context) { + BitbucketSCMSourceContext ctx = (BitbucketSCMSourceContext) context; + ctx.wantBranches(true); + ctx.withAuthority(new BranchSCMHeadAuthority()); + switch (strategyId) { + case 1: + ctx.wantOriginPRs(true); + ctx.withFilter(new ExcludeOriginPRBranchesSCMHeadFilter()); + break; + case 2: + ctx.wantOriginPRs(true); + ctx.withFilter(new OnlyOriginPRBranchesSCMHeadFilter()); + break; + case 3: + default: + // we don't care if it is a PR or not, we're taking them all, no need to ask for PRs and no need + // to filter + break; + + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean includeCategory(@NonNull SCMHeadCategory category) { + return category.isUncategorized(); + } + + /** + * Our descriptor. + */ + @Extension + @Discovery + public static class DescriptorImpl extends SCMSourceTraitDescriptor { + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Discover branches"; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isApplicableToContext(@NonNull Class contextClass) { + return BitbucketSCMSourceContext.class.isAssignableFrom(contextClass); + } + + /** + * Populates the strategy options. + * + * @return the stategy options. + */ + @NonNull + @Restricted(NoExternalUse.class) + @SuppressWarnings("unused") // stapler + public ListBoxModel doFillStrategyIdItems() { + ListBoxModel result = new ListBoxModel(); + result.add(Messages.BranchDiscoveryTrait_excludePRs(), "1"); + result.add(Messages.BranchDiscoveryTrait_onlyPRs(), "2"); + result.add(Messages.BranchDiscoveryTrait_allBranches(), "3"); + return result; + } + } + + /** + * Trusts branches from the origin repository. + */ + public static class BranchSCMHeadAuthority extends SCMHeadAuthority { + /** + * {@inheritDoc} + */ + @Override + protected boolean checkTrusted(@NonNull SCMSourceRequest request, @NonNull BranchSCMHead head) { + return true; + } + + /** + * Out descriptor. + */ + @Extension + public static class DescriptorImpl extends SCMHeadAuthorityDescriptor { + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Trust origin branches"; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isApplicableToOrigin(@NonNull Class originClass) { + return SCMHeadOrigin.Default.class.isAssignableFrom(originClass); + } + } + } + + /** + * Filter that excludes branches that are also filed as a pull request. + */ + public static class ExcludeOriginPRBranchesSCMHeadFilter extends SCMHeadFilter { + /** + * {@inheritDoc} + */ + @Override + public boolean isExcluded(@NonNull SCMSourceRequest request, @NonNull SCMHead head) { + if (head instanceof BranchSCMHead && request instanceof BitbucketSCMSourceRequest) { + BitbucketSCMSourceRequest req = (BitbucketSCMSourceRequest) request; + String fullName = req.getRepoOwner() + "/" + req.getRepository(); + for (BitbucketPullRequest pullRequest : req.getPullRequests()) { + BitbucketRepository source = pullRequest.getSource().getRepository(); + if (fullName.equalsIgnoreCase(source.getFullName()) + && pullRequest.getSource().getBranch().getName().equals(head.getName())) { + return true; + } + } + } + return false; + } + } + + /** + * Filter that excludes branches that are not also filed as a pull request. + */ + public static class OnlyOriginPRBranchesSCMHeadFilter extends SCMHeadFilter { + /** + * {@inheritDoc} + */ + @Override + public boolean isExcluded(@NonNull SCMSourceRequest request, @NonNull SCMHead head) { + if (head instanceof BranchSCMHead && request instanceof BitbucketSCMSourceRequest) { + BitbucketSCMSourceRequest req = (BitbucketSCMSourceRequest) request; + String fullName = req.getRepoOwner() + "/" + req.getRepository(); + for (BitbucketPullRequest pullRequest : req.getPullRequests()) { + BitbucketRepository source = pullRequest.getSource().getRepository(); + if (fullName.equalsIgnoreCase(source.getFullName()) + && pullRequest.getSource().getBranch().getName().equals(head.getName())) { + return false; + } + } + return true; + } + return false; + } + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait.java new file mode 100644 index 000000000..097ffd491 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait.java @@ -0,0 +1,342 @@ +/* + * The MIT License + * + * Copyright (c) 2017, 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 edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.util.ListBoxModel; +import java.io.IOException; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import jenkins.scm.api.SCMHeadCategory; +import jenkins.scm.api.SCMHeadOrigin; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; +import jenkins.scm.api.mixin.ChangeRequestSCMHead2; +import jenkins.scm.api.trait.SCMHeadAuthority; +import jenkins.scm.api.trait.SCMHeadAuthorityDescriptor; +import jenkins.scm.api.trait.SCMSourceContext; +import jenkins.scm.api.trait.SCMSourceRequest; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMSourceTraitDescriptor; +import jenkins.scm.impl.ChangeRequestSCMHeadCategory; +import jenkins.scm.impl.trait.Discovery; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * A {@link Discovery} trait for bitbucket that will discover pull requests from forks of the repository. + * + * @since 2.2.0 + */ +public class ForkPullRequestDiscoveryTrait extends SCMSourceTrait { + /** + * The strategy encoded as a bit-field. + */ + private final int strategyId; + /** + * The authority. + */ + @NonNull + private final SCMHeadAuthority< + ? super BitbucketSCMSourceRequest, + ? extends ChangeRequestSCMHead2, + ? extends SCMRevision> trust; + + /** + * Constructor for stapler. + * + * @param strategyId the strategy id. + * @param trust the authority to use. + */ + @DataBoundConstructor + public ForkPullRequestDiscoveryTrait(int strategyId, + @NonNull SCMHeadAuthority trust) { + this.strategyId = strategyId; + this.trust = trust; + } + + /** + * Constructor for programmatic instantiation. + * + * @param strategies the {@link ChangeRequestCheckoutStrategy} instances. + * @param trust the authority. + */ + public ForkPullRequestDiscoveryTrait(@NonNull EnumSet strategies, + @NonNull SCMHeadAuthority trust) { + this((strategies.contains(ChangeRequestCheckoutStrategy.MERGE) ? 1 : 0) + + (strategies.contains(ChangeRequestCheckoutStrategy.HEAD) ? 2 : 0), trust); + } + + /** + * Gets the strategy id. + * + * @return the strategy id. + */ + public int getStrategyId() { + return strategyId; + } + + /** + * Gets the authority. + * + * @return the authority. + */ + @NonNull + public SCMHeadAuthority getTrust() { + return trust; + } + + /** + * {@inheritDoc} + */ + @Override + protected void decorateContext(SCMSourceContext context) { + BitbucketSCMSourceContext ctx = (BitbucketSCMSourceContext) context; + ctx.wantForkPRs(true); + ctx.withAuthority(trust); + if ((strategyId & 1) != 0) { + ctx.withForkPRStrategies(Collections.singleton(ChangeRequestCheckoutStrategy.MERGE)); + } + if ((strategyId & 2) != 0) { + ctx.withForkPRStrategies(Collections.singleton(ChangeRequestCheckoutStrategy.HEAD)); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean includeCategory(@NonNull SCMHeadCategory category) { + return category instanceof ChangeRequestSCMHeadCategory; + } + + /** + * Our descriptor. + */ + @Extension + @Discovery + public static class DescriptorImpl extends SCMSourceTraitDescriptor { + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return Messages.ForkPullRequestDiscoveryTrait_displayName(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isApplicableToContext(@NonNull Class contextClass) { + return BitbucketSCMSourceContext.class.isAssignableFrom(contextClass); + } + + /** + * Populates the strategy options. + * + * @return the stategy options. + */ + @NonNull + @Restricted(NoExternalUse.class) + @SuppressWarnings("unused") // stapler + public ListBoxModel doFillStrategyIdItems() { + ListBoxModel result = new ListBoxModel(); + result.add(Messages.ForkPullRequestDiscoveryTrait_mergeOnly(), "1"); + result.add(Messages.ForkPullRequestDiscoveryTrait_headOnly(), "2"); + result.add(Messages.ForkPullRequestDiscoveryTrait_headAndMerge(), "3"); + return result; + } + + /** + * Returns the list of appropriate {@link SCMHeadAuthorityDescriptor} instances. + * + * @return the list of appropriate {@link SCMHeadAuthorityDescriptor} instances. + */ + @NonNull + @SuppressWarnings("unused") // stapler + public List getTrustDescriptors() { + return SCMHeadAuthority._for( + BitbucketSCMSourceRequest.class, + PullRequestSCMHead.class, + PullRequestSCMRevision.class, + SCMHeadOrigin.Fork.class + ); + } + + /** + * Returns the default trust for new instances of {@link ForkPullRequestDiscoveryTrait}. + * + * @return the default trust for new instances of {@link ForkPullRequestDiscoveryTrait}. + */ + @NonNull + @SuppressWarnings("unused") // stapler + public SCMHeadAuthority getDefaultTrust() { + return new TrustTeamForks(); + } + } + + /** + * An {@link SCMHeadAuthority} that trusts nothing. + */ + public static class TrustNobody extends SCMHeadAuthority { + /** + * Constructor. + */ + @DataBoundConstructor + public TrustNobody() { + } + + /** + * {@inheritDoc} + */ + @Override + public boolean checkTrusted(@NonNull SCMSourceRequest request, @NonNull ChangeRequestSCMHead2 head) { + return false; + } + + /** + * Our descriptor. + */ + @Extension + public static class DescriptorImpl extends SCMHeadAuthorityDescriptor { + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return Messages.ForkPullRequestDiscoveryTrait_nobodyDisplayName(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isApplicableToOrigin(@NonNull Class originClass) { + return SCMHeadOrigin.Fork.class.isAssignableFrom(originClass); + } + } + } + + /** + * An {@link SCMHeadAuthority} that trusts forks belonging to the same account. + */ + public static class TrustTeamForks + extends SCMHeadAuthority { + + /** + * Constructor. + */ + @DataBoundConstructor + public TrustTeamForks() { + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean checkTrusted(@NonNull BitbucketSCMSourceRequest request, @NonNull PullRequestSCMHead head) + throws IOException, InterruptedException { + if (!head.getOrigin().equals(SCMHeadOrigin.DEFAULT)) { + return head.getRepoOwner().equalsIgnoreCase(request.getRepoOwner()); + } + return false; + } + + /** + * Our descriptor. + */ + @Extension + public static class DescriptorImpl extends SCMHeadAuthorityDescriptor { + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return Messages.ForkPullRequestDiscoveryTrait_teamDisplayName(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isApplicableToOrigin(@NonNull Class originClass) { + return SCMHeadOrigin.Fork.class.isAssignableFrom(originClass); + } + + } + } + + /** + * An {@link SCMHeadAuthority} that trusts everyone. + */ + public static class TrustEveryone extends SCMHeadAuthority { + /** + * Constructor. + */ + @DataBoundConstructor + public TrustEveryone() { + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean checkTrusted(@NonNull SCMSourceRequest request, @NonNull ChangeRequestSCMHead2 head) { + return !head.getOrigin().equals(SCMHeadOrigin.DEFAULT); + } + + /** + * Our descriptor. + */ + @Extension + public static class DescriptorImpl extends SCMHeadAuthorityDescriptor { + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return Messages.ForkPullRequestDiscoveryTrait_everyoneDisplayName(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isApplicableToOrigin(@NonNull Class originClass) { + return SCMHeadOrigin.Fork.class.isAssignableFrom(originClass); + } + } + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/LazyIterable.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/LazyIterable.java new file mode 100644 index 000000000..b12305b76 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/LazyIterable.java @@ -0,0 +1,40 @@ +/* + * The MIT License + * + * Copyright (c) 2017, 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 java.util.Iterator; + +abstract class LazyIterable implements Iterable { + private Iterable delegate; + + protected abstract Iterable create(); + + @Override + public synchronized Iterator iterator() { + if (delegate == null) { + delegate = create(); + } + return delegate.iterator(); + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/MergeWithGitSCMExtension.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/MergeWithGitSCMExtension.java new file mode 100644 index 000000000..75812002a --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/MergeWithGitSCMExtension.java @@ -0,0 +1,128 @@ +/* + * The MIT License + * + * Copyright (c) 2017, 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 edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.git.GitException; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.Revision; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.impl.PreBuildMerge; +import hudson.plugins.git.util.MergeRecord; +import java.io.IOException; +import org.apache.commons.lang.StringUtils; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.jenkinsci.plugins.gitclient.CheckoutCommand; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.jenkinsci.plugins.gitclient.MergeCommand; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * Similar to {@link PreBuildMerge}, but we cannot use that unmodified: we need to specify the exact base branch + * hash. The hash is specified so that we are not subject to a race condition between the {@code baseHash} we think + * we are merging with and a possibly newer one that was just pushed. + * + * @since 2.2.0 + */ +@Restricted(NoExternalUse.class) +public class MergeWithGitSCMExtension extends GitSCMExtension { + @NonNull + private final String baseName; + @CheckForNull + private final String baseHash; + + MergeWithGitSCMExtension(@NonNull String baseName, @CheckForNull String baseHash) { + this.baseName = baseName; + this.baseHash = baseHash; + } + + @NonNull + public String getBaseName() { + return baseName; + } + + public String getBaseHash() { + return baseHash; + } + + @Override + public Revision decorateRevisionToBuild(GitSCM scm, Run build, GitClient git, TaskListener listener, + Revision marked, Revision rev) throws + + IOException, InterruptedException, GitException { + ObjectId baseObjectId; + if (StringUtils.isBlank(baseHash)) { + try { + baseObjectId = git.revParse(Constants.R_REFS + baseName); + } catch (GitException e) { + listener.getLogger().printf("Unable to determine head revision of %s prior to merge with PR%n", + baseName); + throw e; + } + } else { + baseObjectId = ObjectId.fromString(baseHash); + } + listener.getLogger().printf("Merging %s commit %s into PR head commit %s%n", + baseName, baseObjectId.name(), rev.getSha1String() + ); + checkout(scm, build, git, listener, rev); + try { + /* could parse out of JenkinsLocationConfiguration.get().getAdminAddress() but seems overkill */ + git.setAuthor("Jenkins", "nobody@nowhere"); + git.setCommitter("Jenkins", "nobody@nowhere"); + MergeCommand cmd = git.merge().setRevisionToMerge(baseObjectId); + for (GitSCMExtension ext : scm.getExtensions()) { + // By default we do a regular merge, allowing it to fast-forward. + ext.decorateMergeCommand(scm, build, git, listener, cmd); + } + cmd.execute(); + } catch (GitException x) { + // Try to revert merge conflict markers. + // TODO IGitAPI offers a reset(hard) method yet GitClient does not. Why? + checkout(scm, build, git, listener, rev); + // TODO would be nicer to throw an AbortException with just the message, but this is actually worse + // until git-client 1.19.7+ + throw x; + } + build.addAction( + new MergeRecord(baseName, baseObjectId.getName())); // does not seem to be used, but just in case + ObjectId mergeRev = git.revParse(Constants.HEAD); + listener.getLogger().println("Merge succeeded, producing " + mergeRev.name()); + return new Revision(mergeRev, rev.getBranches()); // note that this ensures Build.revision != Build.marked + } + + private void checkout(GitSCM scm, Run build, GitClient git, TaskListener listener, Revision rev) + throws InterruptedException, IOException, GitException { + CheckoutCommand checkoutCommand = git.checkout().ref(rev.getSha1String()); + for (GitSCMExtension ext : scm.getExtensions()) { + ext.decorateCheckoutCommand(scm, build, git, listener, checkoutCommand); + } + checkoutCommand.execute(); + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait.java new file mode 100644 index 000000000..ae1a8b4bd --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait.java @@ -0,0 +1,188 @@ +/* + * The MIT License + * + * Copyright (c) 2017, 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 edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.util.ListBoxModel; +import java.util.Collections; +import java.util.EnumSet; +import jenkins.scm.api.SCMHeadCategory; +import jenkins.scm.api.SCMHeadOrigin; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; +import jenkins.scm.api.mixin.ChangeRequestSCMHead2; +import jenkins.scm.api.trait.SCMHeadAuthority; +import jenkins.scm.api.trait.SCMHeadAuthorityDescriptor; +import jenkins.scm.api.trait.SCMSourceContext; +import jenkins.scm.api.trait.SCMSourceRequest; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMSourceTraitDescriptor; +import jenkins.scm.impl.ChangeRequestSCMHeadCategory; +import jenkins.scm.impl.trait.Discovery; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * A {@link Discovery} trait for bitbucket that will discover pull requests originating from a branch in the repository + * itself. + * + * @since 2.2.0 + */ +public class OriginPullRequestDiscoveryTrait extends SCMSourceTrait { + /** + * The strategy encoded as a bit-field. + */ + private int strategyId; + + /** + * Constructor for stapler. + * + * @param strategyId the strategy id. + */ + @DataBoundConstructor + public OriginPullRequestDiscoveryTrait(int strategyId) { + this.strategyId = strategyId; + } + + /** + * Constructor for programmatic instantiation. + * + * @param strategies the {@link ChangeRequestCheckoutStrategy} instances. + */ + public OriginPullRequestDiscoveryTrait(EnumSet strategies) { + this((strategies.contains(ChangeRequestCheckoutStrategy.MERGE) ? 1 : 0) + + (strategies.contains(ChangeRequestCheckoutStrategy.HEAD) ? 2 : 0)); + } + + /** + * Gets the strategy id. + * + * @return the strategy id. + */ + public int getStrategyId() { + return strategyId; + } + + /** + * {@inheritDoc} + */ + @Override + protected void decorateContext(SCMSourceContext context) { + BitbucketSCMSourceContext ctx = (BitbucketSCMSourceContext) context; + ctx.wantOriginPRs(true); + ctx.withAuthority(new OriginChangeRequestSCMHeadAuthority()); + if ((strategyId & 1) != 0) { + ctx.withOriginPRStrategies(Collections.singleton(ChangeRequestCheckoutStrategy.MERGE)); + } + if ((strategyId & 2) != 0) { + ctx.withOriginPRStrategies(Collections.singleton(ChangeRequestCheckoutStrategy.HEAD)); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean includeCategory(@NonNull SCMHeadCategory category) { + return category instanceof ChangeRequestSCMHeadCategory; + } + + /** + * Our descriptor. + */ + @Extension + @Discovery + public static class DescriptorImpl extends SCMSourceTraitDescriptor { + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Discover pull requests from origin"; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isApplicableToContext(@NonNull Class contextClass) { + return BitbucketSCMSourceContext.class.isAssignableFrom(contextClass); + } + + /** + * Populates the strategy options. + * + * @return the stategy options. + */ + @NonNull + @Restricted(NoExternalUse.class) + @SuppressWarnings("unused") // stapler + public ListBoxModel doFillStrategyIdItems() { + ListBoxModel result = new ListBoxModel(); + result.add(Messages.ForkPullRequestDiscoveryTrait_mergeOnly(), "1"); + result.add(Messages.ForkPullRequestDiscoveryTrait_headOnly(), "2"); + result.add(Messages.ForkPullRequestDiscoveryTrait_headAndMerge(), "3"); + return result; + } + } + + /** + * A {@link SCMHeadAuthority} that trusts origin pull requests + */ + public static class OriginChangeRequestSCMHeadAuthority + extends SCMHeadAuthority { + /** + * {@inheritDoc} + */ + @Override + protected boolean checkTrusted(@NonNull SCMSourceRequest request, @NonNull ChangeRequestSCMHead2 head) { + return SCMHeadOrigin.DEFAULT.equals(head.getOrigin()); + } + + /** + * Our descriptor. + */ + @Extension + public static class DescriptorImpl extends SCMHeadAuthorityDescriptor { + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return Messages.OriginPullRequestDiscoveryTrait_authorityDisplayName(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isApplicableToOrigin(@NonNull Class originClass) { + return SCMHeadOrigin.Default.class.isAssignableFrom(originClass); + } + } + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTrait.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTrait.java new file mode 100644 index 000000000..1ffc94203 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTrait.java @@ -0,0 +1,79 @@ +package com.cloudbees.jenkins.plugins.bitbucket; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import jenkins.scm.api.trait.SCMSourceContext; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMSourceTraitDescriptor; +import jenkins.scm.impl.trait.Discovery; +import org.kohsuke.stapler.DataBoundConstructor; + +/* + * The MIT License + * + * Copyright (c) 2017, 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. + */ + +/** + * A {@link SCMSourceTrait} that supresses all pull requests if the repository is public. + * + * @since 2.2.0 + */ +public class PublicRepoPullRequestFilterTrait extends SCMSourceTrait { + /** + * Constructor. + */ + @DataBoundConstructor + public PublicRepoPullRequestFilterTrait() { + } + + /** + * {@inheritDoc} + */ + @Override + protected void decorateContext(SCMSourceContext context) { + ((BitbucketSCMSourceContext) context).skipPublicPRs(true); + } + + /** + * Our descriptor. + */ + @Extension + @Discovery + public static class DescriptorImpl extends SCMSourceTraitDescriptor { + /** + * {@inheritDoc} + */ + @NonNull + @Override + public String getDisplayName() { + return Messages.PublicRepoPullRequestFilterTrait_displayName(); + } + + /** + * {@inheritDoc} + */ + @Override + public Class getContextClass() { + return BitbucketSCMSourceContext.class; + } + } +} 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 9871607a9..116aae865 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PullRequestSCMHead.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PullRequestSCMHead.java @@ -30,12 +30,11 @@ import hudson.Extension; import java.io.ObjectStreamException; import jenkins.plugins.git.AbstractGitSCMSource; +import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMHeadMigration; import jenkins.scm.api.SCMHeadOrigin; import jenkins.scm.api.SCMRevision; import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; -import jenkins.scm.api.mixin.ChangeRequestSCMHead; -import jenkins.scm.api.SCMHead; import jenkins.scm.api.mixin.ChangeRequestSCMHead2; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.DoNotUse; @@ -43,6 +42,7 @@ /** * {@link SCMHead} for a BitBucket Pull request + * * @since FIXME */ public class PullRequestSCMHead extends SCMHead implements ChangeRequestSCMHead2 { @@ -63,15 +63,40 @@ public class PullRequestSCMHead extends SCMHead implements ChangeRequestSCMHead2 private final SCMHeadOrigin origin; - public PullRequestSCMHead(String repoOwner, String repository, String branchName, - String number, BranchSCMHead target, SCMHeadOrigin origin) { - super(PR_BRANCH_PREFIX + number); + private final ChangeRequestCheckoutStrategy strategy; + + public PullRequestSCMHead(String name, String repoOwner, String repository, String branchName, + String number, BranchSCMHead target, SCMHeadOrigin origin, + ChangeRequestCheckoutStrategy strategy) { + super(name); this.repoOwner = repoOwner; this.repository = repository; this.branchName = branchName; this.number = number; this.target = target; this.origin = origin; + this.strategy = strategy; + } + + public PullRequestSCMHead(String name, String repoOwner, String repository, BitbucketRepositoryType repositoryType, + String branchName, BitbucketPullRequest pr, SCMHeadOrigin origin, + ChangeRequestCheckoutStrategy strategy) { + super(name); + this.repoOwner = repoOwner; + this.repository = repository; + this.branchName = branchName; + this.number = pr.getId(); + this.target = new BranchSCMHead(pr.getDestination().getBranch().getName(), repositoryType); + this.origin = origin; + this.strategy = strategy; + } + + @Deprecated + @Restricted(DoNotUse.class) + public PullRequestSCMHead(String repoOwner, String repository, String branchName, + String number, BranchSCMHead target, SCMHeadOrigin origin) { + this(PR_BRANCH_PREFIX + number, repoOwner, repository, branchName, number, target, origin, + ChangeRequestCheckoutStrategy.HEAD); } @Deprecated @@ -89,18 +114,17 @@ public PullRequestSCMHead(String repoOwner, String repository, String branchName @Deprecated @Restricted(DoNotUse.class) - public PullRequestSCMHead(String repoOwner, String repository, BitbucketRepositoryType repositoryType, String branchName, BitbucketPullRequest pr) { + public PullRequestSCMHead(String repoOwner, String repository, BitbucketRepositoryType repositoryType, + String branchName, BitbucketPullRequest pr) { this(repoOwner, repository, repositoryType, branchName, pr, null); } - public PullRequestSCMHead(String repoOwner, String repository, BitbucketRepositoryType repositoryType, String branchName, BitbucketPullRequest pr, SCMHeadOrigin origin) { - super(PR_BRANCH_PREFIX + pr.getId()); - this.repoOwner = repoOwner; - this.repository = repository; - this.branchName = branchName; - this.number = pr.getId(); - this.target = new BranchSCMHead(pr.getDestination().getBranch().getName(), repositoryType); - this.origin = origin; + @Deprecated + @Restricted(DoNotUse.class) + public PullRequestSCMHead(String repoOwner, String repository, BitbucketRepositoryType repositoryType, + String branchName, BitbucketPullRequest pr, SCMHeadOrigin origin) { + this(PR_BRANCH_PREFIX + pr.getId(), repoOwner, repository, repositoryType, branchName, pr, origin, + ChangeRequestCheckoutStrategy.HEAD); } @SuppressFBWarnings("SE_PRIVATE_READ_RESOLVE_NOT_INHERITED") // because JENKINS-41313 @@ -109,9 +133,9 @@ private Object readResolve() throws ObjectStreamException { // this was a migration during upgrade to 2.0.0 but has not been rebuilt yet, let's see if we can fix it return new SCMHeadWithOwnerAndRepo.PR(repoOwner, repository, getBranchName(), number, target); } - if (origin == null) { - // this was a pre-2.2.0 head, let's see if we can populate the origin details - return new FixOrigin(this); + if (origin == null || strategy == null) { + // this was a pre-2.2.0 head, let's see if we can populate the origin / strategy details + return new FixLegacy(this); } return this; } @@ -147,7 +171,7 @@ public SCMHead getTarget() { @NonNull @Override public ChangeRequestCheckoutStrategy getCheckoutStrategy() { - return ChangeRequestCheckoutStrategy.HEAD; // TODO add support + return strategy; } @NonNull @@ -165,16 +189,17 @@ public SCMHeadOrigin getOrigin() { /** * Used to handle data migration. * - * @see FixOriginMigration1 - * @see FixOriginMigration2 + * @see FixLegacyMigration1 + * @see FixLegacyMigration2 * @deprecated used for data migration. */ @Deprecated @Restricted(NoExternalUse.class) - public static class FixOrigin extends PullRequestSCMHead { + public static class FixLegacy extends PullRequestSCMHead { - FixOrigin(PullRequestSCMHead copy) { - super(copy.repoOwner, copy.repository, copy.branchName, copy.number, copy.target, null); + FixLegacy(PullRequestSCMHead copy) { + super(copy.getName(), copy.repoOwner, copy.repository, copy.branchName, copy.number, + copy.target, copy.getOrigin(), ChangeRequestCheckoutStrategy.HEAD); } } @@ -185,33 +210,36 @@ public static class FixOrigin extends PullRequestSCMHead { */ @Restricted(NoExternalUse.class) @Extension - public static class FixOriginMigration1 extends - SCMHeadMigration { - public FixOriginMigration1() { - super(BitbucketSCMSource.class, FixOrigin.class, AbstractGitSCMSource.SCMRevisionImpl.class); + public static class FixLegacyMigration1 extends + SCMHeadMigration { + public FixLegacyMigration1() { + super(BitbucketSCMSource.class, FixLegacy.class, AbstractGitSCMSource.SCMRevisionImpl.class); } @Override - public PullRequestSCMHead migrate(@NonNull BitbucketSCMSource source, @NonNull FixOrigin head) { + public PullRequestSCMHead migrate(@NonNull BitbucketSCMSource source, @NonNull FixLegacy head) { return new PullRequestSCMHead( head.getName(), + head.getRepoOwner(), head.getRepository(), head.getBranchName(), head.getId(), (BranchSCMHead) head.getTarget(), - source.getRepoOwner().equalsIgnoreCase(head.getRepoOwner()) - ? SCMHeadOrigin.DEFAULT - : new SCMHeadOrigin.Fork(head.getRepoOwner()) + source.originOf(head.getRepoOwner(), head.getRepository()), + ChangeRequestCheckoutStrategy.HEAD // legacy is always HEAD ); } @Override public SCMRevision migrate(@NonNull BitbucketSCMSource source, @NonNull AbstractGitSCMSource.SCMRevisionImpl revision) { - PullRequestSCMHead head = migrate(source, (FixOrigin) revision.getHead()); - return head != null ? new AbstractGitSCMSource.SCMRevisionImpl( - head, - revision.getHash() + PullRequestSCMHead head = migrate(source, (FixLegacy) revision.getHead()); + 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), + new AbstractGitSCMSource.SCMRevisionImpl(head, revision.getHash() + ) ) : null; } } @@ -223,33 +251,36 @@ public SCMRevision migrate(@NonNull BitbucketSCMSource source, */ @Restricted(NoExternalUse.class) @Extension - public static class FixOriginMigration2 extends - SCMHeadMigration { - public FixOriginMigration2() { - super(BitbucketSCMSource.class, FixOrigin.class, BitbucketSCMSource.MercurialRevision.class); + public static class FixLegacyMigration2 extends + SCMHeadMigration { + public FixLegacyMigration2() { + super(BitbucketSCMSource.class, FixLegacy.class, BitbucketSCMSource.MercurialRevision.class); } @Override - public PullRequestSCMHead migrate(@NonNull BitbucketSCMSource source, @NonNull FixOrigin head) { + public PullRequestSCMHead migrate(@NonNull BitbucketSCMSource source, @NonNull FixLegacy head) { return new PullRequestSCMHead( head.getName(), + head.getRepoOwner(), head.getRepository(), head.getBranchName(), head.getId(), (BranchSCMHead) head.getTarget(), - source.getRepoOwner().equalsIgnoreCase(head.getRepoOwner()) - ? SCMHeadOrigin.DEFAULT - : new SCMHeadOrigin.Fork(head.getRepoOwner()) + source.originOf(head.getRepoOwner(), head.getRepository()), + ChangeRequestCheckoutStrategy.HEAD ); } @Override public SCMRevision migrate(@NonNull BitbucketSCMSource source, @NonNull BitbucketSCMSource.MercurialRevision revision) { - PullRequestSCMHead head = migrate(source, (FixOrigin) revision.getHead()); - return head != null ? new BitbucketSCMSource.MercurialRevision( + PullRequestSCMHead head = migrate(source, (FixLegacy) revision.getHead()); + return head != null ? new PullRequestSCMRevision<>( head, - revision.getHash() + // ChangeRequestCheckoutStrategy.HEAD means we ignore the target revision + // so we can leave it null as a placeholder + new BitbucketSCMSource.MercurialRevision(head.getTarget(), null), + new BitbucketSCMSource.MercurialRevision(head, revision.getHash()) ) : 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 new file mode 100644 index 000000000..15e4185a8 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PullRequestSCMRevision.java @@ -0,0 +1,103 @@ +/* + * The MIT License + * + * Copyright 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 edu.umd.cs.findbugs.annotations.NonNull; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; +import jenkins.scm.api.mixin.ChangeRequestSCMRevision; + +/** + * Revision of a pull request. + * + * @since 2.2.0 + */ +public class PullRequestSCMRevision extends ChangeRequestSCMRevision { + + /** + * Standardize serialization. + */ + private static final long serialVersionUID = 1L; + + /** + * The pull head revision. + */ + @NonNull + private final R pull; + + /** + * Constructor. + * + * @param head the head. + * @param target the target revision. + * @param pull the pull revision. + */ + public PullRequestSCMRevision(@NonNull PullRequestSCMHead head, @NonNull R target, @NonNull R pull) { + super(head, target); + this.pull = pull; + } + + /** + * Gets the pull revision. + * + * @return the pull revision. + */ + @NonNull + public R getPull() { + return pull; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equivalent(ChangeRequestSCMRevision o) { + if (!(o instanceof PullRequestSCMRevision)) { + return false; + } + PullRequestSCMRevision other = (PullRequestSCMRevision) o; + return getHead().equals(other.getHead()) && pull.equals(other.pull); + } + + /** + * {@inheritDoc} + */ + @Override + public int _hashCode() { + return pull.hashCode(); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return getHead() instanceof PullRequestSCMHead + && ((PullRequestSCMHead) getHead()).getCheckoutStrategy() == ChangeRequestCheckoutStrategy.MERGE + ? getPull().toString() + "+" + getTarget().toString() + : getPull().toString(); + } + +} 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 e9c9f58a6..56e094bf8 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SCMHeadWithOwnerAndRepo.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SCMHeadWithOwnerAndRepo.java @@ -40,8 +40,8 @@ import jenkins.plugins.git.AbstractGitSCMSource; import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMHeadMigration; -import jenkins.scm.api.SCMHeadOrigin; import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; import jenkins.scm.api.mixin.ChangeRequestSCMHead; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -108,8 +108,8 @@ private static Map getTargets(BitbucketSCMSource source) { Map targets = new HashMap<>(); try { final BitbucketApi bitbucket = BitbucketApiFactory.newInstance( - source.getBitbucketServerUrl(), - source.getScanCredentials(), + source.getServerUrl(), + source.credentials(), source.getRepoOwner(), source.getRepository() ); @@ -136,7 +136,7 @@ public HgMigrationImpl() { } @Override - public SCMHead migrate(@NonNull BitbucketSCMSource source, @NonNull PR head) { + public PullRequestSCMHead migrate(@NonNull BitbucketSCMSource source, @NonNull PR head) { Map targets = getTargets(source); String target = targets.get(head.getId()); if (target == null) { @@ -144,19 +144,30 @@ public SCMHead migrate(@NonNull BitbucketSCMSource source, @NonNull PR head) { head.getId()); target = "\u0000"; } - return new PullRequestSCMHead(head.getRepoOwner(), head.getRepository(), head.getBranchName(), head.getId(), + return new PullRequestSCMHead( + head.getName(), + head.getRepoOwner(), + head.getRepository(), + head.getBranchName(), + head.getId(), new BranchSCMHead(target, BitbucketRepositoryType.MERCURIAL), - source.getRepoOwner().equalsIgnoreCase(head.getRepoOwner()) - ? SCMHeadOrigin.DEFAULT - : new SCMHeadOrigin.Fork(head.getRepoOwner())); + source.originOf(head.getRepoOwner(), head.getRepository()), + ChangeRequestCheckoutStrategy.HEAD + ); } @Override public SCMRevision migrate(@NonNull BitbucketSCMSource source, @NonNull BitbucketSCMSource.MercurialRevision revision) { - SCMHead head = migrate(source, (PR) revision.getHead()); - return head != null ? new BitbucketSCMSource.MercurialRevision(head, revision.getHash()) : null; + PullRequestSCMHead head = migrate(source, (PR) revision.getHead()); + return head != null ? new PullRequestSCMRevision<>( + head, + // ChangeRequestCheckoutStrategy.HEAD means we ignore the target revision + // so we can leave it null as a placeholder + new BitbucketSCMSource.MercurialRevision(head.getTarget(), null), + new BitbucketSCMSource.MercurialRevision(head, revision.getHash()) + ) : null; } } @@ -169,7 +180,7 @@ public GitMigrationImpl() { } @Override - public SCMHead migrate(@NonNull BitbucketSCMSource source, @NonNull PR head) { + public PullRequestSCMHead migrate(@NonNull BitbucketSCMSource source, @NonNull PR head) { Map targets = getTargets(source); String target = targets.get(head.getId()); if (target == null) { @@ -177,18 +188,29 @@ public SCMHead migrate(@NonNull BitbucketSCMSource source, @NonNull PR head) { head.getId()); target = "\u0000"; } - return new PullRequestSCMHead(head.getRepoOwner(), head.getRepository(), head.getBranchName(), head.getId(), + return new PullRequestSCMHead( + head.getName(), + head.getRepoOwner(), + head.getRepository(), + head.getBranchName(), + head.getId(), new BranchSCMHead(target, BitbucketRepositoryType.GIT), - source.getRepoOwner().equalsIgnoreCase(head.getRepoOwner()) - ? SCMHeadOrigin.DEFAULT - : new SCMHeadOrigin.Fork(head.getRepoOwner())); + source.originOf(head.getRepoOwner(), head.getRepository()), + ChangeRequestCheckoutStrategy.HEAD + ); } @Override public SCMRevision migrate(@NonNull BitbucketSCMSource source, @NonNull AbstractGitSCMSource.SCMRevisionImpl revision) { - SCMHead head = migrate(source, (PR) revision.getHead()); - return head != null ? new AbstractGitSCMSource.SCMRevisionImpl(head, revision.getHash()) : null; + PullRequestSCMHead head = migrate(source, (PR) revision.getHead()); + 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), + new AbstractGitSCMSource.SCMRevisionImpl(head, revision.getHash() + ) + ) : null; } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait.java new file mode 100644 index 000000000..171d75e81 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait.java @@ -0,0 +1,181 @@ +/* + * The MIT License + * + * Copyright (c) 2017, 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.sshcredentials.SSHUserPrivateKey; +import com.cloudbees.plugins.credentials.CredentialsMatchers; +import com.cloudbees.plugins.credentials.common.StandardListBoxModel; +import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; +import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.Util; +import hudson.model.Item; +import hudson.model.Queue; +import hudson.model.queue.Tasks; +import hudson.plugins.git.GitSCM; +import hudson.plugins.mercurial.MercurialSCM; +import hudson.plugins.mercurial.MercurialSCMBuilder; +import hudson.scm.SCMDescriptor; +import hudson.security.ACL; +import hudson.util.ListBoxModel; +import jenkins.model.Jenkins; +import jenkins.plugins.git.GitSCMBuilder; +import jenkins.scm.api.trait.SCMBuilder; +import jenkins.scm.api.trait.SCMSourceContext; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMSourceTraitDescriptor; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.AncestorInPath; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; + +/** + * A {@link SCMSourceTrait} for {@link BitbucketSCMSource} that causes the {@link GitSCM} or {@link MercurialSCM} + * checkout to be performed using a SSH private key rather than the Bitbucket username password credentials used + * for scanning / indexing. + * + * @since 2.2.0 + */ +public class SSHCheckoutTrait extends SCMSourceTrait { + + /** + * Credentials for actual clone; may be SSH private key. + */ + @CheckForNull + private final String credentialsId; + + /** + * Constructor. + * + * @param credentialsId the {@link SSHUserPrivateKey#getId()} of the credentials to use or + * {@link BitbucketSCMSource.DescriptorImpl#ANONYMOUS} to + */ + @DataBoundConstructor + public SSHCheckoutTrait(@CheckForNull String credentialsId) { + if (BitbucketSCMSource.DescriptorImpl.ANONYMOUS.equals(credentialsId)) { + // legacy migration of "magic" credential ID. + this.credentialsId = null; + } else { + this.credentialsId = Util.fixEmpty(credentialsId); + } + } + + /** + * Returns the configured credentials id. + * + * @return the configured credentials id or {@code null} to use the build agent's key. + */ + @CheckForNull + public final String getCredentialsId() { + return credentialsId; + } + + /** + * {@inheritDoc} + */ + @Override + protected void decorateBuilder(SCMBuilder builder) { + if (builder instanceof GitSCMBuilder) { + ((GitSCMBuilder) builder).withCredentials(credentialsId); + } else if (builder instanceof MercurialSCMBuilder) { + ((MercurialSCMBuilder) builder).withCredentialsId(credentialsId); + } + } + + /** + * Our descriptor. + */ + @Extension + public static class DescriptorImpl extends SCMSourceTraitDescriptor { + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public String getDisplayName() { + return Messages.SSHCheckoutTrait_displayName(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isApplicableToContext(@NonNull Class contextClass) { + return BitbucketSCMSourceContext.class.isAssignableFrom(contextClass); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isApplicableToBuilder(@NonNull Class builderClass) { + return BitbucketGitSCMBuilder.class.isAssignableFrom(builderClass) + || BitbucketHgSCMBuilder.class.isAssignableFrom(builderClass); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isApplicableToSCM(@NonNull SCMDescriptor scm) { + return scm instanceof GitSCM.DescriptorImpl || scm instanceof MercurialSCM.DescriptorImpl; + } + + /** + * Form completion. + * + * @param context the context. + * @param serverUrl the server url. + * @param credentialsId the current selection. + * @return the form items. + */ + @Restricted(NoExternalUse.class) + @SuppressWarnings("unused") // stapler form binding + public ListBoxModel doFillCredentialsIdItems(@CheckForNull @AncestorInPath Item context, + @QueryParameter String serverUrl, + @QueryParameter String credentialsId) { + if (context == null + ? !Jenkins.getActiveInstance().hasPermission(Jenkins.ADMINISTER) + : !context.hasPermission(Item.EXTENDED_READ)) { + return new StandardListBoxModel().includeCurrentValue(credentialsId); + } + StandardListBoxModel result = new StandardListBoxModel(); + result.add(Messages.SSHCheckoutTrait_useAgentKey(), ""); + return result.includeMatchingAs( + context instanceof Queue.Task + ? Tasks.getDefaultAuthenticationOf((Queue.Task) context) + : ACL.SYSTEM, + context, + StandardUsernameCredentials.class, + URIRequirementBuilder.fromUri(serverUrl).build(), + CredentialsMatchers.instanceOf(SSHUserPrivateKey.class) + ); + } + + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistration.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistration.java new file mode 100644 index 000000000..40fb6f216 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistration.java @@ -0,0 +1,47 @@ +/* + * The MIT License + * + * Copyright (c) 2017, 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.endpoints.BitbucketEndpointConfiguration; + +/** + * Enumeration of the different webhook registration modes. + * + * @since 2.2.0 + */ +public enum WebhookRegistration { + /** + * Disable webhook registration. + */ + DISABLE, + /** + * Use the global system configuration for webhook registration. (If the {@link BitbucketEndpointConfiguration} + * does not have webhook registration configured then this will be the same as {@link #DISABLE}) + */ + SYSTEM, + /** + * Use the item scoped credentials to register the webhook. + */ + ITEM +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait.java new file mode 100644 index 000000000..4549c1f1b --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait.java @@ -0,0 +1,125 @@ +/* + * The MIT License + * + * Copyright (c) 2017, 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.endpoints.BitbucketEndpointConfiguration; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.util.ListBoxModel; +import jenkins.scm.api.trait.SCMSourceContext; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMSourceTraitDescriptor; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * A {@link SCMSourceTrait} for {@link BitbucketSCMSource} that overrides the {@link BitbucketEndpointConfiguration} + * settings for webhook registration. + * + * @since 2.2.0 + */ +public class WebhookRegistrationTrait extends SCMSourceTrait { + + /** + * The mode of registration to apply. + */ + @NonNull + private final WebhookRegistration mode; + + /** + * Constructor. + * + * @param mode the mode of registration to apply. + */ + @DataBoundConstructor + public WebhookRegistrationTrait(@NonNull String mode) { + this(WebhookRegistration.valueOf(mode)); + } + + /** + * Constructor. + * + * @param mode the mode of registration to apply. + */ + public WebhookRegistrationTrait(@NonNull WebhookRegistration mode) { + this.mode = mode; + } + + /** + * Gets the mode of registration to apply. + * + * @return the mode of registration to apply. + */ + @NonNull + public final WebhookRegistration getMode() { + return mode; + } + + /** + * {@inheritDoc} + */ + @Override + protected void decorateContext(SCMSourceContext context) { + ((BitbucketSCMSourceContext) context).webhookRegistration(getMode()); + } + + /** + * Our constructor. + */ + @Extension + public static class DescriptorImpl extends SCMSourceTraitDescriptor { + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return Messages.WebhookRegistrationTrait_displayName(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isApplicableToContext(@NonNull Class contextClass) { + return BitbucketSCMSourceContext.class.isAssignableFrom(contextClass); + } + + /** + * Form completion. + * + * @return the mode options. + */ + @Restricted(NoExternalUse.class) + @SuppressWarnings("unused") // stapler form binding + public ListBoxModel doFillModeItems() { + ListBoxModel result = new ListBoxModel(); + result.add(Messages.WebhookRegistrationTrait_disableHook(), WebhookRegistration.DISABLE.toString()); + result.add(Messages.WebhookRegistrationTrait_useItemHook(), WebhookRegistration.ITEM.toString()); + return result; + } + + } +} 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 9283f51f6..5b744a807 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 @@ -25,12 +25,16 @@ import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSourceContext; import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMHead; +import com.cloudbees.jenkins.plugins.bitbucket.PullRequestSCMRevision; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketHref; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequestEvent; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryType; import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudWebhookPayload; import com.cloudbees.jenkins.plugins.bitbucket.client.events.BitbucketCloudPullRequestEvent; +import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerWebhookPayload; import com.cloudbees.jenkins.plugins.bitbucket.server.events.BitbucketServerPullRequestEvent; import edu.umd.cs.findbugs.annotations.NonNull; @@ -40,17 +44,21 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.plugins.git.AbstractGitSCMSource; import jenkins.scm.api.SCMEvent; import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMHeadEvent; +import jenkins.scm.api.SCMHeadObserver; import jenkins.scm.api.SCMHeadOrigin; import jenkins.scm.api.SCMNavigator; import jenkins.scm.api.SCMRevision; import jenkins.scm.api.SCMSource; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; import static com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType.PULL_REQUEST_DECLINED; import static com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType.PULL_REQUEST_MERGED; @@ -90,17 +98,14 @@ public boolean isMatch(@NonNull SCMNavigator navigator) { return false; } BitbucketSCMNavigator bbNav = (BitbucketSCMNavigator) navigator; - if (!isBitbucketServerUrlMatch(bbNav.getBitbucketServerUrl())) { + if (!isServerUrlMatch(bbNav.getBitbucketServerUrl())) { return false; } - if (!bbNav.getRepoOwner().equalsIgnoreCase(getPayload().getRepository().getOwnerName())) { - return false; - } - return true; + return bbNav.getRepoOwner().equalsIgnoreCase(getPayload().getRepository().getOwnerName()); } - private boolean isBitbucketServerUrlMatch(String serverUrl) { - if (serverUrl == null) { + private boolean isServerUrlMatch(String serverUrl) { + if (BitbucketCloudEndpoint.SERVER_URL.equals(serverUrl)) { // this is a Bitbucket cloud navigator if (getPayload() instanceof BitbucketServerPullRequestEvent) { return false; @@ -144,7 +149,7 @@ public Map heads(@NonNull SCMSource source) { return Collections.emptyMap(); } BitbucketSCMSource src = (BitbucketSCMSource) source; - if (!isBitbucketServerUrlMatch(src.getBitbucketServerUrl())) { + if (!isServerUrlMatch(src.getServerUrl())) { return Collections.emptyMap(); } if (!src.getRepoOwner().equalsIgnoreCase(getPayload().getRepository().getOwnerName())) { @@ -160,37 +165,76 @@ public Map heads(@NonNull SCMSource source) { getPayload().getRepository().getScm()); return Collections.emptyMap(); } - Map result = new HashMap<>(1); - PullRequestSCMHead head = new PullRequestSCMHead( - getPayload().getPullRequest().getSource().getRepository().getOwnerName(), - getPayload().getPullRequest().getSource().getRepository().getRepositoryName(), - type, - getPayload().getPullRequest().getSource().getBranch().getName(), - getPayload().getPullRequest(), - ((BitbucketSCMSource) source).getRepoOwner().equalsIgnoreCase( - getPayload().getPullRequest().getSource().getRepository().getOwnerName() - ) - ? SCMHeadOrigin.DEFAULT - : new SCMHeadOrigin.Fork( - getPayload().getPullRequest().getSource().getRepository().getOwnerName() - ) - ); - if (hookEvent == PULL_REQUEST_DECLINED || hookEvent == PULL_REQUEST_MERGED) { - // special case for repo being deleted - result.put(head, null); - } else { - switch (type) { - case GIT: - result.put(head, new AbstractGitSCMSource.SCMRevisionImpl(head, - getPayload().getPullRequest().getSource().getCommit().getHash())); - break; - case MERCURIAL: - result.put(head, new BitbucketSCMSource.MercurialRevision(head, - getPayload().getPullRequest().getSource().getCommit().getHash())); - break; - default: - LOGGER.log(Level.INFO, "Received event for unknown repository type: {0}", type); - break; + BitbucketSCMSourceContext ctx = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()) + .withTraits(src.getTraits()); + if (!ctx.wantPRs()) { + // doesn't want PRs, let the push event handle origin branches + return Collections.emptyMap(); + } + BitbucketPullRequest pull = getPayload().getPullRequest(); + String pullRepoOwner = pull.getSource().getRepository().getOwnerName(); + String pullRepository = pull.getSource().getRepository().getRepositoryName(); + SCMHeadOrigin headOrigin = src.originOf(pullRepoOwner, pullRepository); + Set strategies = + headOrigin == SCMHeadOrigin.DEFAULT + ? ctx.originPRStrategies() + : ctx.forkPRStrategies(); + Map result = new HashMap<>(strategies.size()); + for (ChangeRequestCheckoutStrategy strategy : strategies) { + String branchName = "PR-" + pull.getId(); + if (strategies.size() > 1) { + branchName = branchName + "-" + strategy.name().toLowerCase(Locale.ENGLISH); + } + PullRequestSCMHead head = new PullRequestSCMHead( + branchName, + pullRepoOwner, + pullRepository, + type, + pull.getSource().getBranch().getName(), + pull, + headOrigin, + strategy + ); + if (hookEvent == PULL_REQUEST_DECLINED || hookEvent == PULL_REQUEST_MERGED) { + // special case for repo being deleted + result.put(head, null); + } else { + String targetHash = + pull.getDestination().getCommit().getHash(); + String pullHash = pull.getSource().getCommit().getHash(); + switch (type) { + case GIT: + result.put(head, new PullRequestSCMRevision<>( + head, + new AbstractGitSCMSource.SCMRevisionImpl( + head.getTarget(), + targetHash + ), + new AbstractGitSCMSource.SCMRevisionImpl( + head, + pullHash + ) + ) + ); + break; + case MERCURIAL: + result.put(head, new PullRequestSCMRevision<>( + head, + new BitbucketSCMSource.MercurialRevision( + head.getTarget(), + targetHash + ), + new BitbucketSCMSource.MercurialRevision( + head, + pullHash + ) + ) + ); + break; + default: + LOGGER.log(Level.INFO, "Received event for unknown repository type: {0}", type); + break; + } } } return result; diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PushHookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PushHookProcessor.java index deea45dee..9262f8842 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PushHookProcessor.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PushHookProcessor.java @@ -32,11 +32,9 @@ import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudWebhookPayload; import com.cloudbees.jenkins.plugins.bitbucket.client.events.BitbucketCloudPushEvent; import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerWebhookPayload; - import com.cloudbees.jenkins.plugins.bitbucket.server.events.BitbucketServerPushEvent; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.scm.SCM; -import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.Collections; @@ -52,7 +50,6 @@ import jenkins.scm.api.SCMNavigator; import jenkins.scm.api.SCMRevision; import jenkins.scm.api.SCMSource; -import org.codehaus.jackson.map.ObjectMapper; public class PushHookProcessor extends HookProcessor { @@ -92,16 +89,13 @@ public boolean isMatch(@NonNull SCMNavigator navigator) { return false; } BitbucketSCMNavigator bbNav = (BitbucketSCMNavigator) navigator; - if (!isBitbucketServerUrlMatch(bbNav.getBitbucketServerUrl())) { - return false; - } - if (!bbNav.getRepoOwner().equalsIgnoreCase(getPayload().getRepository().getOwnerName())) { + if (!isServerUrlMatch(bbNav.getServerUrl())) { return false; } - return true; + return bbNav.getRepoOwner().equalsIgnoreCase(getPayload().getRepository().getOwnerName()); } - private boolean isBitbucketServerUrlMatch(String serverUrl) { + private boolean isServerUrlMatch(String serverUrl) { if (serverUrl == null) { // this is a Bitbucket cloud navigator if (getPayload() instanceof BitbucketServerPushEvent) { @@ -146,7 +140,7 @@ public Map heads(@NonNull SCMSource source) { return Collections.emptyMap(); } BitbucketSCMSource src = (BitbucketSCMSource) source; - if (!isBitbucketServerUrlMatch(src.getBitbucketServerUrl())) { + if (!isServerUrlMatch(src.getServerUrl())) { return Collections.emptyMap(); } if (!src.getRepoOwner().equalsIgnoreCase(getPayload().getRepository().getOwnerName())) { diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookAutoRegisterListener.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookAutoRegisterListener.java index db32dd506..cfbd05216 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookAutoRegisterListener.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookAutoRegisterListener.java @@ -23,7 +23,22 @@ */ package com.cloudbees.jenkins.plugins.bitbucket.hooks; +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSourceContext; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApiFactory; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook; +import com.cloudbees.jenkins.plugins.bitbucket.client.repository.BitbucketRepositoryHook; +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.server.client.repository.BitbucketServerWebhook; +import hudson.Extension; +import hudson.model.Item; +import hudson.model.listeners.ItemListener; +import hudson.triggers.SafeTimerTask; +import hudson.util.DaemonThreadFactory; +import hudson.util.NamingThreadFactory; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -35,19 +50,8 @@ import java.util.concurrent.Executors; import java.util.logging.Level; import java.util.logging.Logger; - -import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook; -import com.cloudbees.jenkins.plugins.bitbucket.client.repository.BitbucketRepositoryHook; - -import hudson.Extension; -import hudson.model.Item; -import hudson.model.listeners.ItemListener; -import hudson.triggers.SafeTimerTask; -import hudson.util.DaemonThreadFactory; -import hudson.util.NamingThreadFactory; import jenkins.model.Jenkins; +import jenkins.scm.api.SCMHeadObserver; import jenkins.scm.api.SCMSource; import jenkins.scm.api.SCMSourceOwner; import jenkins.scm.api.SCMSourceOwners; @@ -95,7 +99,15 @@ public void onUpdated(Item item) { } private boolean isApplicable(Item item) { - return item instanceof SCMSourceOwner; + if (!(item instanceof SCMSourceOwner)) { + return false; + } + for (SCMSource source : ((SCMSourceOwner) item).getSCMSources()) { + if (source instanceof BitbucketSCMSource) { + return true; + } + } + return false; } private void registerHooksAsync(final SCMSourceOwner owner) { @@ -127,16 +139,22 @@ public void doRun() { // synchronized just to avoid duplicated webhooks in case SCMSourceOwner is updated repeteadly and quickly private synchronized void registerHooks(SCMSourceOwner owner) throws IOException, InterruptedException { String rootUrl = Jenkins.getActiveInstance().getRootUrl(); + List sources = getBitucketSCMSources(owner); + if (sources.isEmpty()) { + // don't spam logs if we are irrelevant + return; + } if (rootUrl != null && !rootUrl.startsWith("http://localhost")) { - List sources = getBitucketSCMSources(owner); for (BitbucketSCMSource source : sources) { - if (source.isAutoRegisterHook()) { - BitbucketApi bitbucket = source.buildBitbucketClient(); + BitbucketApi bitbucket = bitbucketApiFor(source); + if (bitbucket != null) { List existent = bitbucket.getWebHooks(); BitbucketWebHook existing = null; + String hookReceiverUrl = + Jenkins.getActiveInstance().getRootUrl() + BitbucketSCMSourcePushHookReceiver.FULL_PATH; for (BitbucketWebHook hook : existent) { // Check if there is a hook pointing to us already - if (hook.getUrl().equals(Jenkins.getActiveInstance().getRootUrl() + BitbucketSCMSourcePushHookReceiver.FULL_PATH)) { + if (hookReceiverUrl.equals(hook.getUrl())) { existing = hook; break; } @@ -151,21 +169,42 @@ private synchronized void registerHooks(SCMSourceOwner owner) throws IOException bitbucket.registerCommitWebHook(existing); } } else if (existing == null) { - LOGGER.info(String.format("Registering hook for %s/%s", source.getRepoOwner(), source.getRepository())); - bitbucket.registerCommitWebHook(getHook(source)); + LOGGER.info(String.format("Registering hook for %s/%s", source.getRepoOwner(), + source.getRepository())); + bitbucket.registerCommitWebHook(getHook(source)); } } } } else { - LOGGER.warning(String.format("Can not register hook. Jenkins root URL is not valid: %s", rootUrl)); + // only complain about being unable to register the hook if someone wants the hook registered. + SOURCES: + for (BitbucketSCMSource source : sources) { + switch (new BitbucketSCMSourceContext(null, SCMHeadObserver.none()) + .withTraits(source.getTraits()) + .webhookRegistration()) { + case DISABLE: + continue SOURCES; + case SYSTEM: + AbstractBitbucketEndpoint endpoint = + BitbucketEndpointConfiguration.get().findEndpoint(source.getServerUrl()); + if (endpoint == null || !endpoint.isManageHooks()) { + continue SOURCES; + } + break; + case ITEM: + break; + } + LOGGER.warning(String.format("Can not register hook. Jenkins root URL is not valid: %s", rootUrl)); + return; + } } } private void removeHooks(SCMSourceOwner owner) throws IOException, InterruptedException { List sources = getBitucketSCMSources(owner); for (BitbucketSCMSource source : sources) { - if (source.isAutoRegisterHook()) { - BitbucketApi bitbucket = source.buildBitbucketClient(); + BitbucketApi bitbucket = bitbucketApiFor(source); + if (bitbucket != null) { List existent = bitbucket.getWebHooks(); BitbucketWebHook hook = null; for (BitbucketWebHook h : existent) { @@ -186,6 +225,30 @@ private void removeHooks(SCMSourceOwner owner) throws IOException, InterruptedEx } } + private BitbucketApi bitbucketApiFor(BitbucketSCMSource source) { + switch (new BitbucketSCMSourceContext(null, SCMHeadObserver.none()) + .withTraits(source.getTraits()) + .webhookRegistration()) { + case DISABLE: + return null; + case SYSTEM: + AbstractBitbucketEndpoint endpoint = + BitbucketEndpointConfiguration.get().findEndpoint(source.getServerUrl()); + return endpoint == null || !endpoint.isManageHooks() + ? null + : BitbucketApiFactory.newInstance( + endpoint.getServerUrl(), + endpoint.credentials(), + source.getRepoOwner(), + source.getRepository() + ); + case ITEM: + return source.buildBitbucketClient(); + default: + return null; + } + } + private boolean isUsedSomewhereElse(SCMSourceOwner owner, String repoOwner, String repoName) { Iterable all = SCMSourceOwners.all(); for (SCMSourceOwner other : all) { @@ -213,7 +276,7 @@ private List getBitucketSCMSources(SCMSourceOwner owner) { } private BitbucketWebHook getHook(BitbucketSCMSource owner) { - if (owner.getBitbucketServerUrl() == null) { + if (BitbucketCloudEndpoint.SERVER_URL.equals(owner.getServerUrl())) { BitbucketRepositoryHook hook = new BitbucketRepositoryHook(); hook.setActive(true); hook.setDescription("Jenkins hook"); 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 5854048e7..3211612b3 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 @@ -1,30 +1,25 @@ - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-autoRegisterHooks.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-autoRegisterHooks.jelly deleted file mode 100644 index 9a4993a3e..000000000 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-autoRegisterHooks.jelly +++ /dev/null @@ -1,19 +0,0 @@ - - - -
-

- Activate this option to auto-register a hook on all discovered Bitbucket Cloud repositories. This hook will notify Jenkins - about new commits on branches and pull requests, so new builds will be triggered automatically on related jobs. -

-

- Otherwise the hook can be created manually using the following information: -

    -
  • URL: ${app.rootUrl != null ? app.rootUrl : "[JENKINS_ROOT_URL]/"}bitbucket-scmsource-hook/notify
  • -
  • Check "Push", "Pull Request Created" and "Pull Request Updated" in the triggers section.
  • -
- NOTE: this Jenkins instance must accesible somehow from internet (concretely reachable from Bitbucket Cloud). -

-
-
-
\ No newline at end of file diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-bitbucketServerUrl.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-bitbucketServerUrl.html deleted file mode 100644 index e6af74405..000000000 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-bitbucketServerUrl.html +++ /dev/null @@ -1,5 +0,0 @@ -
-

Left blank to use Bitbucket Cloud. - Set your Bitbucket Server base URL to use your own server instance. The URL must contain the full URL including - a base path (if exists).

-
\ No newline at end of file diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-checkoutCredentialsId.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-checkoutCredentialsId.html deleted file mode 100644 index 5a4902357..000000000 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-checkoutCredentialsId.html +++ /dev/null @@ -1,3 +0,0 @@ -
-

Credentials used to check out sources during a build.

-
\ No newline at end of file diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-credentialsId.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-credentialsId.html index 1c45a1376..81762e037 100644 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-credentialsId.html +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-credentialsId.html @@ -1,3 +1,3 @@
-

Credentials used to scan branches and check out sources

-
\ No newline at end of file + Credentials used to scan branches (also the default credentials to use when checking out sources) + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-pattern.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-pattern.html deleted file mode 100644 index c9f8d59d5..000000000 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-pattern.html +++ /dev/null @@ -1,3 +0,0 @@ -
-

Regular expression to specify what repositories one wants to include

-
\ No newline at end of file diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-repoOwner.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-repoOwner.html index 21fd44a40..61a823e16 100644 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-repoOwner.html +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-repoOwner.html @@ -1,11 +1,11 @@

Specify the name of the Bitbucket Team or Bitbucket User Account.

- It could be a Bitbucket Project also, if using Bitbucket Server. - In this case (Bitbucket Server): + It could be a Bitbucket Project also, if using Bitbucket Server. + In this case (Bitbucket Server):

    -
  • Use the project key, not the project name.
  • -
  • If using a user account instead of a project, add a "~" character before the username, i.e. "~joe". +
  • Use the project key, not the project name.
  • +
  • If using a user account instead of a project, add a "~" character before the username, i.e. "~joe".

-
\ No newline at end of file + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-serverUrl.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-serverUrl.html new file mode 100644 index 000000000..473b322d6 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-serverUrl.html @@ -0,0 +1,5 @@ +
+ The server to connect to. The list of servers is configured in the Manage Jenkins » Configure + Jenkins › Bitbucket Endpoints screen. The list of servers can include both Bitbucket Cloud as well as + Bitbucket Server instances. +
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-traits.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-traits.html new file mode 100644 index 000000000..bbb3e0b9e --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/help-traits.html @@ -0,0 +1,27 @@ +
+ The behaviours control what is discovered from the Bitbucket server. The behaviours are grouped into a number + of categories: +
+
Repository
+
These behaviours determine what repositories get discovered. Only repositories that have at least one + discovered branch / pull request can themselves be discovered. +
+
Within repository
+
These behaviours determine what gets discovered within each repository. If you do not configure + at least one discovery behaviour then nothing will be found!
+
General
+
These behaviours affect the configuration of each discovered branch / pull request. These behaviours + are relevant to both Git and Mercurial based repositories +
+
Git
+
These behaviours affect the configuration of each discovered branch / pull request + if and only if the repository is a Git repository. If the repository is a Mercurial + repository, these behaviours will be silently ignored. +
+
Mercurial
+
These behaviours affect the configuration of each discovered branch / pull request + if and only if the repository is a Mercurial repository. If the repository is a Git + repository, these behaviours will be silently ignored. +
+
+
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 ffb43b5fc..29c4cd147 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 @@ -1,30 +1,30 @@ - - + + + + + + + + + + + + + + - + + - - + + - - - - - - - - - - - - - - diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-autoRegisterHook.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-autoRegisterHook.jelly deleted file mode 100644 index 21e3fde71..000000000 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-autoRegisterHook.jelly +++ /dev/null @@ -1,19 +0,0 @@ - - - -
-

- Activate this option to auto-register a hook on the specified Bitbucket repository. This hook will notify Jenkins - about new commits on branches and pull requests, so new builds will be triggered automatically on related jobs. -

-

- Otherwise the hook can be created manually using the following information: -

    -
  • URL: [JENKINS_ROOT_URL]/bitbucket-scmsource-hook/notify
  • -
  • Check "Push", "Pull Request Created" and "Pull Request Updated" in the triggers section.
  • -
- NOTE: [JENKINS_ROOT_URL] must be exactly the same that is configured in Jenkins main configuration. -

-
-
-
\ No newline at end of file diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-bitbucketServerUrl.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-bitbucketServerUrl.html deleted file mode 100644 index 5cc96ca08..000000000 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-bitbucketServerUrl.html +++ /dev/null @@ -1,7 +0,0 @@ -
-

- Left blank to use Bitbucket Cloud. - Set your Bitbucket Server base URL to use your own server instance. The URL must contain the full URL including - a base path (if exists). -

-
\ No newline at end of file diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-checkoutCredentialsId.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-checkoutCredentialsId.jelly deleted file mode 100644 index 6e4a8e298..000000000 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-checkoutCredentialsId.jelly +++ /dev/null @@ -1,8 +0,0 @@ - - - -
- Credentials used to clone the repository. They can be Username and Password credentials or SSH credentials -
-
-
\ No newline at end of file diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-credentialsId.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-credentialsId.html new file mode 100644 index 000000000..81762e037 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-credentialsId.html @@ -0,0 +1,3 @@ +
+ Credentials used to scan branches (also the default credentials to use when checking out sources) +
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-credentialsId.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-credentialsId.jelly deleted file mode 100644 index 3e4687723..000000000 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-credentialsId.jelly +++ /dev/null @@ -1,9 +0,0 @@ - - - -
- Credentials used to access Bitbucket REST API to retrieve branches and pull requests information. - They must be Username and Password credentials. -
-
-
\ No newline at end of file diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-repoOwner.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-repoOwner.html new file mode 100644 index 000000000..61a823e16 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-repoOwner.html @@ -0,0 +1,11 @@ +
+

Specify the name of the Bitbucket Team or Bitbucket User Account.

+

+ It could be a Bitbucket Project also, if using Bitbucket Server. + In this case (Bitbucket Server): +

    +
  • Use the project key, not the project name.
  • +
  • If using a user account instead of a project, add a "~" character before the username, i.e. "~joe". +
+

+
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-repoOwner.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-repoOwner.jelly deleted file mode 100644 index 06b46f056..000000000 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-repoOwner.jelly +++ /dev/null @@ -1,8 +0,0 @@ - - - -
- Repository owner. It will be used to build the repository URL: http://bitbucket.org/[owner]/repository -
-
-
\ No newline at end of file diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-repository.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-repository.html new file mode 100644 index 000000000..cf4748277 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-repository.html @@ -0,0 +1,3 @@ +
+ The repository to scan. +
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-serverUrl.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-serverUrl.html new file mode 100644 index 000000000..473b322d6 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-serverUrl.html @@ -0,0 +1,5 @@ +
+ The server to connect to. The list of servers is configured in the Manage Jenkins » Configure + Jenkins › Bitbucket Endpoints screen. The list of servers can include both Bitbucket Cloud as well as + Bitbucket Server instances. +
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-traits.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-traits.html new file mode 100644 index 000000000..7372680fa --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help-traits.html @@ -0,0 +1,23 @@ +
+ The behaviours control what is discovered from the Bitbucket repository. The behaviours are grouped into a number + of categories: +
+
Within repository
+
These behaviours determine what gets discovered. If you do not configure at least one discovery + behaviour then nothing will be found!
+
General
+
These behaviours affect the configuration of each discovered branch / pull request. These behaviours + are relevant to both Git and Mercurial based repositories +
+
Git
+
These behaviours affect the configuration of each discovered branch / pull request + if and only if the repository is a Git repository. If the repository is a Mercurial + repository, these behaviours will be silently ignored. +
+
Mercurial
+
These behaviours affect the configuration of each discovered branch / pull request + if and only if the repository is a Mercurial repository. If the repository is a Git + repository, these behaviours will be silently ignored. +
+
+
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help.html index 95469e763..1769838a1 100644 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help.html +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/help.html @@ -1,10 +1,4 @@
- SCM Source implementation which provides a connector to import branches and pull requests - from Bitbucket. - - It will retrieve all branches and pull requests available at indexing time. Username and password - credentials are needed to access Bitbucket API (Scan Credentials field), and both SSH and username/password - can be used for git checkout (see advanced options by clicking in the Advanced button below). - - If only "Scan Credentials" are provided they will be used for the scan process and git checkout. + Discovers branches and/or pull requests from a specific repository in either Bitbucket Cloud or a Bitbucket Server + instance.
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait/config.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait/config.jelly new file mode 100644 index 000000000..3076844a2 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait/config.jelly @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait/help-strategyId.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait/help-strategyId.html new file mode 100644 index 000000000..1caa2990c --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait/help-strategyId.html @@ -0,0 +1,20 @@ +
+ Determines which branches are discovered. +
+
Exclude branches that are also filed as PRs
+
+ If you are discovering origin pull requests, it may not make sense to discover the same changes both as a + pull request and as a branch. +
+
Only branches that are also filed as PRs
+
+ This option exists to preserve legacy behaviour when upgrading from older versions of the plugin. + NOTE: If you have an actual use case for this option please file a pull request against this text. +
+
All branches
+
+ Ignores whether the branch is also filed as a pull request and instead discovers all branches on the + origin repository. +
+
+
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait/help.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait/help.html new file mode 100644 index 000000000..d3ab0bfdb --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait/help.html @@ -0,0 +1,3 @@ +
+ Discovers branches on the repository. +
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait/config.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait/config.jelly new file mode 100644 index 000000000..47bc583ae --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait/config.jelly @@ -0,0 +1,10 @@ + + + + + + + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait/help-strategyId.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait/help-strategyId.html new file mode 100644 index 000000000..8310b7d80 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait/help-strategyId.html @@ -0,0 +1,15 @@ +
+ Determines how pull requests are discovered: +
    +
  • Discover each pull request once with the discovered revision corresponding to the result of merging with the + current revision of the target branch +
  • +
  • Discover each pull request once with the discovered revision corresponding to the pull request head revision + without merging +
  • +
  • Discover each pull request twice. The first discovered revision corresponds to the result of merging with + the current revision of the target branch in each scan. The second parallel discovered revision corresponds + to the pull request head revision without merging +
  • +
+
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait/help-trust.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait/help-trust.html new file mode 100644 index 000000000..e9b2090cb --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait/help-trust.html @@ -0,0 +1,33 @@ +
+

+ One of the great powers of pull requests is that anyone with read access to a repository can fork it, commit + some changes to their fork and then create a pull request against the original repository with their changes. + There are some files stored in source control that are important. For example, a Jenkinsfile + may contain configuration details to sandbox pull requests in order to mitigate against malicious pull requests. + In order to protect against a malicious pull request itself modifying the Jenkinsfile to remove + the protections, you can define the trust policy for pull requests from forks. +

+

+ Other plugins can extend the available trust policies. The default policies are: +

+
+
Nobody
+
+ Pull requests from forks will all be treated as untrusted. This means that where Jenkins requires a + trusted file (e.g. Jenkinsfile) the contents of that file will be retrieved from the + target branch on the origin repository and not from the pull request branch on the fork repository. +
+
Forks in the same account
+
+ Bitbucket allows for a repository to be forked into a "sibling" repository in the same account but using + a different name. This strategy will trust any pull requests from forks that are in the same account as + the target repository on the basis that users have to have been granted write permission to account in + order create such a fork. +
+
Everyone
+
+ All pull requests from forks will be treated as trusted. NOTE: this option can be dangerous + if used on a public repository hosted on Bitbucket Cloud. +
+
+
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait/help.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait/help.html new file mode 100644 index 000000000..b124cab6f --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait/help.html @@ -0,0 +1,3 @@ +
+ Discovers pull requests where the origin repository is a fork of the target repository. +
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/Messages.properties b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/Messages.properties index 9e34b7aae..3abda79c1 100644 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/Messages.properties +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/Messages.properties @@ -2,12 +2,32 @@ BitbucketLink.DisplayName=Bitbucket BitbucketSCMNavigator.UncategorizedSCMSourceCategory.DisplayName=Repositories BitbucketSCMSource.UncategorizedSCMHeadCategory.DisplayName=Branches BitbucketSCMSource.ChangeRequestSCMHeadCategory.DisplayName=Pull requests +BitbucketSCMSource.NoMatchingOwner=Could not find: {0} +BitbucketSCMSource.UnauthorizedAnonymous=Determination as to whether {0} may or may not exist is not permitted \ + without suitable credentials. +BitbucketSCMSource.UnauthorizedOwner=The selected credentials do not have permission to determine whether {0} does or\ + does not exist. BitbucketSCMNavigator.DisplayName=Bitbucket Team/Project BitbucketSCMNavigator.Description=Scans a Bitbucket Cloud Team (or Bitbucket Server Project) for all repositories matching some defined markers. BitbucketRepoMetadataAction.IconDescription=Bitbucket Repository BitbucketRepoMetadataAction.IconDescription.Git=Bitbucket Git Repository BitbucketRepoMetadataAction.IconDescription.Hg=Bitbucket Mercurial Repository BitbucketTeamMetadataAction.IconDescription=Bitbucket Team/Project -ListViewColumn.Repository=Repository -ListViewColumn.Branch=Branch -ListViewColumn.PullRequest=Pull Request +BranchDiscoveryTrait.allBranches=All branches +BranchDiscoveryTrait.excludePRs=Exclude branches that are also filed as PRs +BranchDiscoveryTrait.onlyPRs=Only branches that are also filed as PRs +ForkPullRequestDiscoveryTrait.displayName=Discover pull requests from forks +ForkPullRequestDiscoveryTrait.everyoneDisplayName=Everyone +ForkPullRequestDiscoveryTrait.headAndMerge=Both the current pull request revision and the pull request merged with \ + the current target branch revision +ForkPullRequestDiscoveryTrait.headOnly=The current pull request revision +ForkPullRequestDiscoveryTrait.mergeOnly=Merging the pull request with the current target branch revision +ForkPullRequestDiscoveryTrait.nobodyDisplayName=Nobody +ForkPullRequestDiscoveryTrait.teamDisplayName=Forks in the same account +OriginPullRequestDiscoveryTrait.authorityDisplayName=Trust origin pull requests +PublicRepoPullRequestFilterTrait.displayName=Exclude pull requests from public repositories +SSHCheckoutTrait.displayName=Checkout over SSH +SSHCheckoutTrait.useAgentKey=- use build agent''s key - +WebhookRegistrationTrait.disableHook=Disable hook management +WebhookRegistrationTrait.displayName=Override hook management +WebhookRegistrationTrait.useItemHook=Use item credentials for hook management diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait/config.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait/config.jelly new file mode 100644 index 000000000..3076844a2 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait/config.jelly @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait/help-strategyId.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait/help-strategyId.html new file mode 100644 index 000000000..8310b7d80 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait/help-strategyId.html @@ -0,0 +1,15 @@ +
+ Determines how pull requests are discovered: +
    +
  • Discover each pull request once with the discovered revision corresponding to the result of merging with the + current revision of the target branch +
  • +
  • Discover each pull request once with the discovered revision corresponding to the pull request head revision + without merging +
  • +
  • Discover each pull request twice. The first discovered revision corresponds to the result of merging with + the current revision of the target branch in each scan. The second parallel discovered revision corresponds + to the pull request head revision without merging +
  • +
+
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait/help.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait/help.html new file mode 100644 index 000000000..bb405c5e2 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait/help.html @@ -0,0 +1,3 @@ +
+ Discovers pull requests where the origin repository is the same as the target repository. +
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTrait/help.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTrait/help.html new file mode 100644 index 000000000..178c06f6a --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTrait/help.html @@ -0,0 +1,5 @@ +
+ If the repository being scanned is a public repository, this behaviour will exclude all pull requests. + (Note: This behaviour is not especially useful if scanning a single repository as you could just not include the + pull request discovery behaviours in the first place) +
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait/config.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait/config.jelly new file mode 100644 index 000000000..10b015195 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait/config.jelly @@ -0,0 +1,7 @@ + + + + + + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait/help-credentialsId.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait/help-credentialsId.html new file mode 100644 index 000000000..f5d1cf4cf --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait/help-credentialsId.html @@ -0,0 +1,3 @@ +
+ Credentials used to check out sources. Must be a SSH key based credential. +
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait/help.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait/help.html new file mode 100644 index 000000000..c36738c4c --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait/help.html @@ -0,0 +1,9 @@ +
+ By default the discovered branches / pull requests will all use the same username / password credentials + that were used for discovery when checking out sources. This means that the checkout will be using the + https:// protocol for the Git / Mercurial repository. +

+ This behaviour allows you to select the SSH private key to be used for checking out sources, which will + consequently force the checkout to use the ssh:// protocol. +

+
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait/config.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait/config.jelly new file mode 100644 index 000000000..194536ed0 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait/config.jelly @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait/help-mode.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait/help-mode.html new file mode 100644 index 000000000..2829c4f0c --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait/help-mode.html @@ -0,0 +1,11 @@ +
+ There are two available modes: +
+
Disable hook management
+
Disables hook management irrespective of the global defaults.
+
Use item credentials for hook management
+
Enabled hook management but uses the selected credentials to manage the hooks rather than those defined in + Manage Jenkins » Configure Jenkins › Bitbucket Endpoints +
+
+
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait/help.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait/help.html new file mode 100644 index 000000000..3124da05d --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait/help.html @@ -0,0 +1,24 @@ +
+

+ Overrides the defaults for webhook management. +

+

+ Webhooks are used to inform Jenkins about changes to repositories. There are two ways webhooks can be + configured: +

+
    +
  • Manual webhook configuration requires the user to configure Bitbucket with the Jenkins URL in order + to ensure that Bitbucket will send the events to Jenkins after every change. +
  • +
  • Automatic webhook configuration requires that Jenkins has credentials with sufficient permission to + configure webhooks and also that Jenkins knows the URL that Bitbucket can connect to. +
  • +
+

+ The Manage Jenkins » Configure Jenkins › Bitbucket Endpoints allows defining the list of + servers. Each server + can be associated with credentials. If credentials are defined then the default behaviour is to use those + credentials to automatically manage the webhooks of all repositories that Jenkins is interested in. If no + credentials are defined then the default behaviour is to require the user to manually configure webhooks. +

+
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 061cb6c48..cdcb58abd 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketClientMockUtils.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketClientMockUtils.java @@ -23,37 +23,34 @@ */ package com.cloudbees.jenkins.plugins.bitbucket; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - +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.api.BitbucketRepositoryType; -import com.cloudbees.jenkins.plugins.bitbucket.client.pullrequest.BitbucketPullRequestValueDestination; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; - -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudApiClient; import com.cloudbees.jenkins.plugins.bitbucket.client.branch.BitbucketCloudBranch; import com.cloudbees.jenkins.plugins.bitbucket.client.branch.BitbucketCloudCommit; import com.cloudbees.jenkins.plugins.bitbucket.client.pullrequest.BitbucketPullRequestValue; -import com.cloudbees.jenkins.plugins.bitbucket.client.pullrequest.BitbucketPullRequestValue.Author; +import com.cloudbees.jenkins.plugins.bitbucket.client.pullrequest.BitbucketPullRequestValueDestination; import com.cloudbees.jenkins.plugins.bitbucket.client.pullrequest.BitbucketPullRequestValueRepository; import com.cloudbees.jenkins.plugins.bitbucket.client.repository.BitbucketCloudRepository; -import com.cloudbees.jenkins.plugins.bitbucket.client.repository.BitbucketRepositoryHook; import com.cloudbees.jenkins.plugins.bitbucket.client.repository.BitbucketCloudTeam; +import com.cloudbees.jenkins.plugins.bitbucket.client.repository.BitbucketRepositoryHook; import com.cloudbees.jenkins.plugins.bitbucket.hooks.BitbucketSCMSourcePushHookReceiver; - import hudson.model.TaskListener; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; import jenkins.model.Jenkins; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + public class BitbucketClientMockUtils { public static BitbucketCloudApiClient getAPIClientMock(BitbucketRepositoryType type, boolean includePullRequests, @@ -74,14 +71,15 @@ public static BitbucketCloudApiClient getAPIClientMock(BitbucketRepositoryType t if (includePullRequests) { when(bitbucket.getPullRequests()).thenReturn(Arrays.asList(getPullRequest())); - when(bitbucket.checkPathExists("my-feature-branch", "markerfile.txt")).thenReturn(true); + when(bitbucket.checkPathExists("e851558f77c098d21af6bb8cc54a423f7cf12147", "markerfile.txt")) + .thenReturn(true); when(bitbucket.resolveSourceFullHash(any(BitbucketPullRequestValue.class))) .thenReturn("e851558f77c098d21af6bb8cc54a423f7cf12147"); } // mock file exists - when(bitbucket.checkPathExists("branch1", "markerfile.txt")).thenReturn(true); - when(bitbucket.checkPathExists("branch2", "markerfile.txt")).thenReturn(false); + when(bitbucket.checkPathExists("52fc8e220d77ec400f7fc96a91d2fd0bb1bc553a", "markerfile.txt")).thenReturn(true); + when(bitbucket.checkPathExists("707c59ce8292c927dddb6807fcf9c3c5e7c9b00f", "markerfile.txt")).thenReturn(false); // Team discovering mocks when(bitbucket.getTeam()).thenReturn(getTeam()); diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilderTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilderTest.java new file mode 100644 index 000000000..42e691de5 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilderTest.java @@ -0,0 +1,2210 @@ +package com.cloudbees.jenkins.plugins.bitbucket; + +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketHref; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryType; +import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey; +import com.cloudbees.plugins.credentials.Credentials; +import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.SystemCredentialsProvider; +import com.cloudbees.plugins.credentials.domains.Domain; +import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.Revision; +import hudson.plugins.git.UserRemoteConfig; +import hudson.plugins.git.browser.BitbucketWeb; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.impl.BuildChooserSetting; +import hudson.util.LogTaskListener; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.branch.BranchSource; +import jenkins.plugins.git.AbstractGitSCMSource; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMHeadOrigin; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; +import org.eclipse.jgit.transport.RemoteConfig; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.mockito.Mockito; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +public class BitbucketGitSCMBuilderTest { + @ClassRule + public static JenkinsRule j = new JenkinsRule(); + private BitbucketSCMSource source; + private WorkflowMultiBranchProject owner; + + @Before + public void setUp() throws IOException { + owner = j.createProject(WorkflowMultiBranchProject.class); + source = new BitbucketSCMSource("test", "tester", "test-repo"); + owner.setSourcesList(Collections.singletonList(new BranchSource(source))); + source.setOwner(owner); + SystemCredentialsProvider.getInstance().setDomainCredentialsMap(Collections.singletonMap(Domain.global(), + Arrays.asList( + new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "user-pass", null, "git-user", + "git-secret"), new BasicSSHUserPrivateKey(CredentialsScope.GLOBAL, "user-key", "git", + new BasicSSHUserPrivateKey.UsersPrivateKeySource(), null, null)))); + } + + @After + public void tearDown() throws IOException, InterruptedException { + SystemCredentialsProvider.getInstance() + .setDomainCredentialsMap(Collections.>emptyMap()); + owner.delete(); + } + + @Test + public void given__cloud_branch_rev_anon__when__build__then__scmBuilt() throws Exception { + BranchSCMHead head = new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT); + 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((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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.org/tester/test-repo.git")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + 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/test-branch:refs/remotes/origin/test-branch")); + assertThat(config.getUrl(), is("https://bitbucket.org/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.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")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), hasSize(1)); + GitSCMExtension extension = actual.getExtensions().get(0); + assertThat(extension, instanceOf(BuildChooserSetting.class)); + BuildChooserSetting chooser = (BuildChooserSetting) extension; + 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__cloud_branch_rev_userpass__when__build__then__scmBuilt() throws Exception { + BranchSCMHead head = new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT); + AbstractGitSCMSource.SCMRevisionImpl revision = + 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(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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.org/tester/test-repo.git")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + 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/test-branch:refs/remotes/origin/test-branch")); + assertThat(config.getUrl(), is("https://bitbucket.org/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.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")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), hasSize(1)); + GitSCMExtension extension = actual.getExtensions().get(0); + assertThat(extension, instanceOf(BuildChooserSetting.class)); + BuildChooserSetting chooser = (BuildChooserSetting) extension; + 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__cloud_branch_rev_userkey__when__build__then__scmBuilt() throws Exception { + BranchSCMHead head = new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT); + AbstractGitSCMSource.SCMRevisionImpl revision = + 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(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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.remote(), is("git@bitbucket.org:tester/test-repo.git")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + 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/test-branch:refs/remotes/origin/test-branch")); + assertThat(config.getUrl(), is("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.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(), hasSize(1)); + GitSCMExtension extension = actual.getExtensions().get(0); + assertThat(extension, instanceOf(BuildChooserSetting.class)); + BuildChooserSetting chooser = (BuildChooserSetting) extension; + 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__cloud_branch_norev_anon__when__build__then__scmBuilt() throws Exception { + BranchSCMHead head = new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT); + 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(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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.org/tester/test-repo.git")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + 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/test-branch:refs/remotes/origin/test-branch")); + assertThat(config.getUrl(), is("https://bitbucket.org/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.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")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), hasSize(0)); + } + + @Test + public void given__cloud_branch_norev_userpass__when__build__then__scmBuilt() throws Exception { + BranchSCMHead head = new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT); + BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, + head, null, "user-pass"); + assertThat(instance.credentialsId(), is("user-pass")); + 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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.org/tester/test-repo.git")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + 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/test-branch:refs/remotes/origin/test-branch")); + assertThat(config.getUrl(), is("https://bitbucket.org/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.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")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), hasSize(0)); + } + + @Test + public void given__cloud_branch_norev_userkey__when__build__then__scmBuilt() throws Exception { + BranchSCMHead head = new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT); + BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, + head, null, "user-key"); + assertThat(instance.credentialsId(), is("user-key")); + 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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.remote(), is("git@bitbucket.org:tester/test-repo.git")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + 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/test-branch:refs/remotes/origin/test-branch")); + assertThat(config.getUrl(), is("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.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(), hasSize(0)); + } + + @Test + public void given__server_branch_rev_anon__when__build__then__scmBuilt() throws Exception { + source.setServerUrl("https://bitbucket.test"); + BranchSCMHead head = new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT); + 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((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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + + 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.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/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(), hasSize(1)); + GitSCMExtension extension = actual.getExtensions().get(0); + assertThat(extension, instanceOf(BuildChooserSetting.class)); + BuildChooserSetting chooser = (BuildChooserSetting) extension; + 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 { + source.setServerUrl("https://bitbucket.test"); + BranchSCMHead head = new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT); + AbstractGitSCMSource.SCMRevisionImpl revision = + 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(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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + + 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.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/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(), hasSize(1)); + GitSCMExtension extension = actual.getExtensions().get(0); + assertThat(extension, instanceOf(BuildChooserSetting.class)); + BuildChooserSetting chooser = (BuildChooserSetting) extension; + 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_userkey__when__build__then__scmBuilt() throws Exception { + source.setServerUrl("https://bitbucket.test"); + BranchSCMHead head = new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT); + AbstractGitSCMSource.SCMRevisionImpl revision = + 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(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"), + 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")); + + 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("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/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(), hasSize(1)); + GitSCMExtension extension = actual.getExtensions().get(0); + assertThat(extension, instanceOf(BuildChooserSetting.class)); + BuildChooserSetting chooser = (BuildChooserSetting) extension; + 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_norev_anon__when__build__then__scmBuilt() throws Exception { + source.setServerUrl("https://bitbucket.test"); + BranchSCMHead head = new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT); + 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(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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + + 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.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/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(), hasSize(0)); + } + + @Test + public void given__server_branch_norev_userpass__when__build__then__scmBuilt() throws Exception { + source.setServerUrl("https://bitbucket.test"); + BranchSCMHead head = new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT); + BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, + head, null, "user-pass"); + assertThat(instance.credentialsId(), is("user-pass")); + 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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + + 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.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/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(), hasSize(0)); + } + + @Test + public void given__server_branch_norev_userkey__when__build__then__scmBuilt() throws Exception { + source.setServerUrl("https://bitbucket.test"); + BranchSCMHead head = new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT); + BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, + head, null, "user-key"); + assertThat(instance.credentialsId(), is("user-key")); + 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"), + 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")); + + 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("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/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(), hasSize(0)); + } + + + @Test + public void given__cloud_pullHead_rev_anon__when__build__then__scmBuilt() throws Exception { + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), 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, 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/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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.org/qa/qa-repo.git")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-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("https://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("https://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")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), hasSize(1)); + GitSCMExtension extension = actual.getExtensions().get(0); + assertThat(extension, instanceOf(BuildChooserSetting.class)); + BuildChooserSetting chooser = (BuildChooserSetting) extension; + 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__cloud_pullHead_rev_userpass__when__build__then__scmBuilt() throws Exception { + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), 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(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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.org/qa/qa-repo.git")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-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("https://bitbucket.org/qa/qa-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.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")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), hasSize(1)); + GitSCMExtension extension = actual.getExtensions().get(0); + assertThat(extension, instanceOf(BuildChooserSetting.class)); + BuildChooserSetting chooser = (BuildChooserSetting) extension; + 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__cloud_pullHead_rev_userkey__when__build__then__scmBuilt() throws Exception { + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), 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(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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.remote(), is("git@bitbucket.org:qa/qa-repo.git")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-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.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.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")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), hasSize(1)); + GitSCMExtension extension = actual.getExtensions().get(0); + assertThat(extension, instanceOf(BuildChooserSetting.class)); + BuildChooserSetting chooser = (BuildChooserSetting) extension; + 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__cloud_pullHead_norev_anon__when__build__then__scmBuilt() throws Exception { + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), 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(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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.org/qa/qa-repo.git")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-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("https://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("https://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")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), hasSize(0)); + } + + @Test + public void given__cloud_pullHead_norev_userpass__when__build__then__scmBuilt() throws Exception { + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), new SCMHeadOrigin.Fork("qa/qa-repo"), + ChangeRequestCheckoutStrategy.HEAD); + BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, + head, null, "user-pass"); + assertThat(instance.credentialsId(), is("user-pass")); + 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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.org/qa/qa-repo.git")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-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("https://bitbucket.org/qa/qa-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.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")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), hasSize(0)); + } + + @Test + public void given__cloud_pullHead_norev_userkey__when__build__then__scmBuilt() throws Exception { + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), new SCMHeadOrigin.Fork("qa/qa-repo"), + ChangeRequestCheckoutStrategy.HEAD); + BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, + head, null, "user-key"); + assertThat(instance.credentialsId(), is("user-key")); + 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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.remote(), is("git@bitbucket.org:qa/qa-repo.git")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-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.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.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")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), hasSize(0)); + } + + @Test + public void given__server_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", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), 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, 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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + + 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)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), hasSize(1)); + GitSCMExtension extension = actual.getExtensions().get(0); + assertThat(extension, instanceOf(BuildChooserSetting.class)); + BuildChooserSetting chooser = (BuildChooserSetting) extension; + 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_userpass__when__build__then__scmBuilt() throws Exception { + source.setServerUrl("https://bitbucket.test"); + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), 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(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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + + 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(), hasSize(1)); + GitSCMExtension extension = actual.getExtensions().get(0); + assertThat(extension, instanceOf(BuildChooserSetting.class)); + BuildChooserSetting chooser = (BuildChooserSetting) extension; + 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", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), 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(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"), + 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")); + + 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(), hasSize(1)); + GitSCMExtension extension = actual.getExtensions().get(0); + assertThat(extension, instanceOf(BuildChooserSetting.class)); + BuildChooserSetting chooser = (BuildChooserSetting) extension; + 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", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), 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(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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + + 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)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), hasSize(0)); + } + + @Test + public void given__server_pullHead_norev_userpass__when__build__then__scmBuilt() throws Exception { + source.setServerUrl("https://bitbucket.test"); + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), new SCMHeadOrigin.Fork("qa/qa-repo"), + ChangeRequestCheckoutStrategy.HEAD); + BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, + head, null, "user-pass"); + assertThat(instance.credentialsId(), is("user-pass")); + 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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + + 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(), hasSize(0)); + } + + @Test + public void given__server_pullHead_norev_userkey__when__build__then__scmBuilt() throws Exception { + source.setServerUrl("https://bitbucket.test"); + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), new SCMHeadOrigin.Fork("qa/qa-repo"), + ChangeRequestCheckoutStrategy.HEAD); + BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, + head, null, "user-key"); + assertThat(instance.credentialsId(), is("user-key")); + 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"), + 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")); + + 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(), hasSize(0)); + } + + + @Test + public void given__cloud_pullMerge_rev_anon__when__build__then__scmBuilt() throws Exception { + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), new SCMHeadOrigin.Fork("qa/qa-repo"), + 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(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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.org/qa/qa-repo.git")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-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("https://bitbucket.org/qa/qa-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.getUrl(), is("https://bitbucket.org/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.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")); + 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.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")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), hasSize(2)); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); + assertThat(chooser, notNullValue()); + 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")); + MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); + assertThat(merge, notNullValue()); + assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); + assertThat(merge.getBaseHash(), is("deadbeefcafebabedeadbeefcafebabedeadbeef")); + } + + @Test + public void given__cloud_pullMerge_rev_userpass__when__build__then__scmBuilt() throws Exception { + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), new SCMHeadOrigin.Fork("qa/qa-repo"), + 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, "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(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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.org/qa/qa-repo.git")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-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("https://bitbucket.org/qa/qa-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.getUrl(), is("https://bitbucket.org/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.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")); + 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.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")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), hasSize(2)); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); + assertThat(chooser, notNullValue()); + 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")); + MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); + assertThat(merge, notNullValue()); + assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); + assertThat(merge.getBaseHash(), is("deadbeefcafebabedeadbeefcafebabedeadbeef")); + } + + @Test + public void given__cloud_pullMerge_rev_userkey__when__build__then__scmBuilt() throws Exception { + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), new SCMHeadOrigin.Fork("qa/qa-repo"), + 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, "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(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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.remote(), is("git@bitbucket.org:qa/qa-repo.git")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-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.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.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.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")); + 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("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")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), hasSize(2)); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); + assertThat(chooser, notNullValue()); + 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")); + MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); + assertThat(merge, notNullValue()); + assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); + assertThat(merge.getBaseHash(), is("deadbeefcafebabedeadbeefcafebabedeadbeef")); + } + + @Test + public void given__cloud_pullMerge_norev_anon__when__build__then__scmBuilt() throws Exception { + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), new SCMHeadOrigin.Fork("qa/qa-repo"), + ChangeRequestCheckoutStrategy.MERGE); + 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(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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.org/qa/qa-repo.git")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-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("https://bitbucket.org/qa/qa-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.getUrl(), is("https://bitbucket.org/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.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")); + 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.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")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), hasSize(1)); + GitSCMExtension extension = actual.getExtensions().get(0); + assertThat(extension, instanceOf(MergeWithGitSCMExtension.class)); + MergeWithGitSCMExtension merge = (MergeWithGitSCMExtension) extension; + assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); + assertThat(merge.getBaseHash(), is(nullValue())); + } + + @Test + public void given__cloud_pullMerge_norev_userpass__when__build__then__scmBuilt() throws Exception { + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), new SCMHeadOrigin.Fork("qa/qa-repo"), + ChangeRequestCheckoutStrategy.MERGE); + BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, + head, null, "user-pass"); + assertThat(instance.credentialsId(), is("user-pass")); + 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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.org/qa/qa-repo.git")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-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("https://bitbucket.org/qa/qa-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.getUrl(), is("https://bitbucket.org/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.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")); + 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.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")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), hasSize(1)); + GitSCMExtension extension = actual.getExtensions().get(0); + assertThat(extension, instanceOf(MergeWithGitSCMExtension.class)); + MergeWithGitSCMExtension merge = (MergeWithGitSCMExtension) extension; + assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); + assertThat(merge.getBaseHash(), is(nullValue())); + } + + @Test + public void given__cloud_pullMerge_norev_userkey__when__build__then__scmBuilt() throws Exception { + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), new SCMHeadOrigin.Fork("qa/qa-repo"), + ChangeRequestCheckoutStrategy.MERGE); + BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, + head, null, "user-key"); + assertThat(instance.credentialsId(), is("user-key")); + 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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.remote(), is("git@bitbucket.org:qa/qa-repo.git")); + + GitSCM actual = instance.build(); + assertThat(actual.getBrowser(), instanceOf(BitbucketWeb.class)); + assertThat(actual.getBrowser().getRepoUrl(), is("https://bitbucket.org/qa/qa-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.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.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.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")); + 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("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")); + assertThat(origin.getFetchRefSpecs().get(0).isForceUpdate(), is(true)); + assertThat(origin.getFetchRefSpecs().get(0).isWildcard(), is(false)); + assertThat(actual.getExtensions(), hasSize(1)); + GitSCMExtension extension = actual.getExtensions().get(0); + assertThat(extension, instanceOf(MergeWithGitSCMExtension.class)); + MergeWithGitSCMExtension merge = (MergeWithGitSCMExtension) extension; + assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); + assertThat(merge.getBaseHash(), is(nullValue())); + } + + @Test + public void given__server_pullMerge_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", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), new SCMHeadOrigin.Fork("qa/qa-repo"), + 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(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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + + 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)); + 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.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)); + 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(actual.getExtensions(), hasSize(2)); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); + assertThat(chooser, notNullValue()); + 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")); + MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); + assertThat(merge, notNullValue()); + assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); + assertThat(merge.getBaseHash(), is("deadbeefcafebabedeadbeefcafebabedeadbeef")); + } + + + @Test + public void given__server_pullMerge_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", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), new SCMHeadOrigin.Fork("qa/qa-repo"), + 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, "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(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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + + 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)); + 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.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)); + 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(actual.getExtensions(), hasSize(2)); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); + assertThat(chooser, notNullValue()); + 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")); + MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); + assertThat(merge, notNullValue()); + assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); + assertThat(merge.getBaseHash(), is("deadbeefcafebabedeadbeefcafebabedeadbeef")); + } + + @Test + public void given__server_pullMerge_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", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), new SCMHeadOrigin.Fork("qa/qa-repo"), + 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, "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(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"), + 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")); + + 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)); + 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.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)); + 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(actual.getExtensions(), hasSize(2)); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); + assertThat(chooser, notNullValue()); + 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")); + MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); + assertThat(merge, notNullValue()); + assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); + assertThat(merge.getBaseHash(), is("deadbeefcafebabedeadbeefcafebabedeadbeef")); + } + + @Test + public void given__server_pullMerge_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", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), new SCMHeadOrigin.Fork("qa/qa-repo"), + ChangeRequestCheckoutStrategy.MERGE); + 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(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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + + 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)); + 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.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)); + 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(actual.getExtensions(), hasSize(1)); + MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); + assertThat(merge, notNullValue()); + assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); + assertThat(merge.getBaseHash(), is(nullValue())); + } + + @Test + public void given__server_pullMerge_norev_userpass__when__build__then__scmBuilt() throws Exception { + source.setServerUrl("https://bitbucket.test"); + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), new SCMHeadOrigin.Fork("qa/qa-repo"), + ChangeRequestCheckoutStrategy.MERGE); + BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, + head, null, "user-pass"); + assertThat(instance.credentialsId(), is("user-pass")); + 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"), + new BitbucketHref("ssh", "ssh://git@bitbucket.test:7999/tester/test-repo.git") + )); + assertThat(instance.remote(), is("https://bitbucket.test/scm/tester/test-repo.git")); + + 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)); + 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.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)); + 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(actual.getExtensions(), hasSize(1)); + MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); + assertThat(merge, notNullValue()); + assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); + assertThat(merge.getBaseHash(), is(nullValue())); + } + + @Test + public void given__server_pullMerge_norev_userkey__when__build__then__scmBuilt() throws Exception { + source.setServerUrl("https://bitbucket.test"); + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.GIT), new SCMHeadOrigin.Fork("qa/qa-repo"), + ChangeRequestCheckoutStrategy.MERGE); + BitbucketGitSCMBuilder instance = new BitbucketGitSCMBuilder(source, + head, null, "user-key"); + assertThat(instance.credentialsId(), is("user-key")); + 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"), + 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")); + + 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)); + 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.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)); + 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(actual.getExtensions(), hasSize(1)); + MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); + assertThat(merge, notNullValue()); + assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); + assertThat(merge.getBaseHash(), is(nullValue())); + } + + private static T getExtension(GitSCM scm, Class type) { + for (GitSCMExtension e : scm.getExtensions()) { + if (type.isInstance(e)) { + return type.cast(e); + } + } + return null; + } + +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketHgSCMBuilderTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketHgSCMBuilderTest.java new file mode 100644 index 000000000..bb3ee40b4 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketHgSCMBuilderTest.java @@ -0,0 +1,456 @@ +package com.cloudbees.jenkins.plugins.bitbucket; + +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketHref; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryType; +import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey; +import com.cloudbees.plugins.credentials.Credentials; +import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.SystemCredentialsProvider; +import com.cloudbees.plugins.credentials.domains.Domain; +import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; +import hudson.plugins.mercurial.MercurialSCM; +import hudson.plugins.mercurial.browser.BitBucket; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import jenkins.branch.BranchSource; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMHeadOrigin; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; +import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +public class BitbucketHgSCMBuilderTest { + @ClassRule + public static JenkinsRule j = new JenkinsRule(); + private BitbucketSCMSource source; + private WorkflowMultiBranchProject owner; + + @Before + public void setUp() throws IOException { + owner = j.createProject(WorkflowMultiBranchProject.class); + source = new BitbucketSCMSource("test", "tester", "test-repo"); + owner.setSourcesList(Collections.singletonList(new BranchSource(source))); + source.setOwner(owner); + SystemCredentialsProvider.getInstance().setDomainCredentialsMap(Collections.singletonMap(Domain.global(), + Arrays.asList( + new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "user-pass", null, "git-user", + "git-secret"), new BasicSSHUserPrivateKey(CredentialsScope.GLOBAL, "user-key", "git", + new BasicSSHUserPrivateKey.UsersPrivateKeySource(), null, null)))); + } + + @After + public void tearDown() throws IOException, InterruptedException { + SystemCredentialsProvider.getInstance() + .setDomainCredentialsMap(Collections.>emptyMap()); + owner.delete(); + } + + @Test + public void given__branch_rev_anon__when__build__then__scmBuilt() throws Exception { + BranchSCMHead head = new BranchSCMHead("test-branch", BitbucketRepositoryType.MERCURIAL); + BitbucketSCMSource.MercurialRevision revision = + new BitbucketSCMSource.MercurialRevision(head, "cafebabedeadbeefcafebabedeadbeefcafebabe"); + BitbucketHgSCMBuilder instance = new BitbucketHgSCMBuilder(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(instance.cloneLinks(), is(Collections.emptyList())); + assertThat("expecting dummy value until clone links provided or withBitbucketSource called", + instance.source(), is("https://bitbucket.org")); + assertThat(instance.browser(), instanceOf(BitBucket.class)); + assertThat(instance.browser().getUrl().toString(), is("https://bitbucket.org/tester/test-repo/")); + + instance.withCloneLinks(Arrays.asList( + new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.source(), is("https://bitbucket.org/tester/test-repo")); + + MercurialSCM actual = instance.build(); + assertThat(actual.getCredentialsId(), is(nullValue())); + assertThat(actual.getBrowser(), instanceOf(BitBucket.class)); + assertThat(actual.getBrowser().getUrl().toString(), is("https://bitbucket.org/tester/test-repo/")); + assertThat(actual.getSource(), is("https://bitbucket.org/tester/test-repo")); + assertThat(actual.getRevisionType(), is(MercurialSCM.RevisionType.CHANGESET)); + assertThat(actual.getRevision(), is("cafebabedeadbeefcafebabedeadbeefcafebabe")); + } + + @Test + public void given__pullHead_rev_anon__when__build__then__scmBuilt() throws Exception { + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.MERCURIAL), + new SCMHeadOrigin.Fork("qa/qa-repo"), + ChangeRequestCheckoutStrategy.HEAD); + PullRequestSCMRevision revision = + new PullRequestSCMRevision<>(head, new BitbucketSCMSource.MercurialRevision(head.getTarget(), + "deadbeefcafebabedeadbeefcafebabedeadbeef"), + new BitbucketSCMSource.MercurialRevision(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); + BitbucketHgSCMBuilder instance = new BitbucketHgSCMBuilder(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(instance.cloneLinks(), is(Collections.emptyList())); + assertThat("expecting dummy value until clone links provided or withBitbucketSource called", + instance.source(), is("https://bitbucket.org")); + assertThat(instance.browser(), instanceOf(BitBucket.class)); + assertThat(instance.browser().getUrl().toString(), is("https://bitbucket.org/tester/test-repo/")); + + instance.withCloneLinks(Arrays.asList( + new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.source(), is("https://bitbucket.org/qa/qa-repo")); + + MercurialSCM actual = instance.build(); + assertThat(actual.getCredentialsId(), is(nullValue())); + assertThat(actual.getBrowser(), instanceOf(BitBucket.class)); + assertThat(actual.getBrowser().getUrl().toString(), is("https://bitbucket.org/qa/qa-repo/")); + assertThat(actual.getSource(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getRevisionType(), is(MercurialSCM.RevisionType.CHANGESET)); + assertThat(actual.getRevision(), is("cafebabedeadbeefcafebabedeadbeefcafebabe")); + } + + @Test + public void given__branch_rev_userpass__when__build__then__scmBuilt() throws Exception { + BranchSCMHead head = new BranchSCMHead("test-branch", BitbucketRepositoryType.MERCURIAL); + BitbucketSCMSource.MercurialRevision revision = + new BitbucketSCMSource.MercurialRevision(head, "cafebabedeadbeefcafebabedeadbeefcafebabe"); + BitbucketHgSCMBuilder instance = new BitbucketHgSCMBuilder(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(instance.cloneLinks(), is(Collections.emptyList())); + assertThat("expecting dummy value until clone links provided or withBitbucketSource called", + instance.source(), is("https://bitbucket.org")); + assertThat(instance.browser(), instanceOf(BitBucket.class)); + assertThat(instance.browser().getUrl().toString(), is("https://bitbucket.org/tester/test-repo/")); + + instance.withCloneLinks(Arrays.asList( + new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.source(), is("https://bitbucket.org/tester/test-repo")); + + MercurialSCM actual = instance.build(); + assertThat(actual.getCredentialsId(), is("user-pass")); + assertThat(actual.getBrowser(), instanceOf(BitBucket.class)); + assertThat(actual.getBrowser().getUrl().toString(), is("https://bitbucket.org/tester/test-repo/")); + assertThat(actual.getSource(), is("https://bitbucket.org/tester/test-repo")); + assertThat(actual.getRevisionType(), is(MercurialSCM.RevisionType.CHANGESET)); + assertThat(actual.getRevision(), is("cafebabedeadbeefcafebabedeadbeefcafebabe")); + } + + @Test + public void given__pullHead_rev_userpass__when__build__then__scmBuilt() throws Exception { + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.MERCURIAL), + new SCMHeadOrigin.Fork("qa/qa-repo"), + ChangeRequestCheckoutStrategy.HEAD); + PullRequestSCMRevision revision = + new PullRequestSCMRevision<>(head, new BitbucketSCMSource.MercurialRevision(head.getTarget(), + "deadbeefcafebabedeadbeefcafebabedeadbeef"), + new BitbucketSCMSource.MercurialRevision(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); + BitbucketHgSCMBuilder instance = new BitbucketHgSCMBuilder(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(instance.cloneLinks(), is(Collections.emptyList())); + assertThat("expecting dummy value until clone links provided or withBitbucketSource called", + instance.source(), is("https://bitbucket.org")); + assertThat(instance.browser(), instanceOf(BitBucket.class)); + assertThat(instance.browser().getUrl().toString(), is("https://bitbucket.org/tester/test-repo/")); + + instance.withCloneLinks(Arrays.asList( + new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.source(), is("https://bitbucket.org/qa/qa-repo")); + + MercurialSCM actual = instance.build(); + assertThat(actual.getCredentialsId(), is("user-pass")); + assertThat(actual.getBrowser(), instanceOf(BitBucket.class)); + assertThat(actual.getBrowser().getUrl().toString(), is("https://bitbucket.org/qa/qa-repo/")); + assertThat(actual.getSource(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getRevisionType(), is(MercurialSCM.RevisionType.CHANGESET)); + assertThat(actual.getRevision(), is("cafebabedeadbeefcafebabedeadbeefcafebabe")); + } + + @Test + public void given__branch_rev_userkey__when__build__then__scmBuilt() throws Exception { + BranchSCMHead head = new BranchSCMHead("test-branch", BitbucketRepositoryType.MERCURIAL); + BitbucketSCMSource.MercurialRevision revision = + new BitbucketSCMSource.MercurialRevision(head, "cafebabedeadbeefcafebabedeadbeefcafebabe"); + BitbucketHgSCMBuilder instance = new BitbucketHgSCMBuilder(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(instance.cloneLinks(), is(Collections.emptyList())); + assertThat("expecting dummy value until clone links provided or withBitbucketSource called", + instance.source(), is("https://bitbucket.org")); + assertThat(instance.browser(), instanceOf(BitBucket.class)); + assertThat(instance.browser().getUrl().toString(), is("https://bitbucket.org/tester/test-repo/")); + + instance.withCloneLinks(Arrays.asList( + new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.source(), is("ssh://hg@bitbucket.org/tester/test-repo")); + + MercurialSCM actual = instance.build(); + assertThat(actual.getCredentialsId(), is("user-key")); + assertThat(actual.getBrowser(), instanceOf(BitBucket.class)); + assertThat(actual.getBrowser().getUrl().toString(), is("https://bitbucket.org/tester/test-repo/")); + assertThat(actual.getSource(), is("ssh://hg@bitbucket.org/tester/test-repo")); + assertThat(actual.getRevisionType(), is(MercurialSCM.RevisionType.CHANGESET)); + assertThat(actual.getRevision(), is("cafebabedeadbeefcafebabedeadbeefcafebabe")); + } + + @Test + public void given__pullHead_rev_userkey__when__build__then__scmBuilt() throws Exception { + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.MERCURIAL), + new SCMHeadOrigin.Fork("qa/qa-repo"), + ChangeRequestCheckoutStrategy.HEAD); + PullRequestSCMRevision revision = + new PullRequestSCMRevision<>(head, new BitbucketSCMSource.MercurialRevision(head.getTarget(), + "deadbeefcafebabedeadbeefcafebabedeadbeef"), + new BitbucketSCMSource.MercurialRevision(head, "cafebabedeadbeefcafebabedeadbeefcafebabe")); + BitbucketHgSCMBuilder instance = new BitbucketHgSCMBuilder(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(instance.cloneLinks(), is(Collections.emptyList())); + assertThat("expecting dummy value until clone links provided or withBitbucketSource called", + instance.source(), is("https://bitbucket.org")); + assertThat(instance.browser(), instanceOf(BitBucket.class)); + assertThat(instance.browser().getUrl().toString(), is("https://bitbucket.org/tester/test-repo/")); + + instance.withCloneLinks(Arrays.asList( + new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.source(), is("ssh://hg@bitbucket.org/qa/qa-repo")); + + MercurialSCM actual = instance.build(); + assertThat(actual.getCredentialsId(), is("user-key")); + assertThat(actual.getBrowser(), instanceOf(BitBucket.class)); + assertThat(actual.getBrowser().getUrl().toString(), is("https://bitbucket.org/qa/qa-repo/")); + assertThat(actual.getSource(), is("ssh://hg@bitbucket.org/qa/qa-repo")); + assertThat(actual.getRevisionType(), is(MercurialSCM.RevisionType.CHANGESET)); + assertThat(actual.getRevision(), is("cafebabedeadbeefcafebabedeadbeefcafebabe")); + } + + @Test + public void given__branch_norev_anon__when__build__then__scmBuilt() throws Exception { + BranchSCMHead head = new BranchSCMHead("test-branch", BitbucketRepositoryType.MERCURIAL); + BitbucketHgSCMBuilder instance = new BitbucketHgSCMBuilder(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(instance.cloneLinks(), is(Collections.emptyList())); + assertThat("expecting dummy value until clone links provided or withBitbucketSource called", + instance.source(), is("https://bitbucket.org")); + assertThat(instance.browser(), instanceOf(BitBucket.class)); + assertThat(instance.browser().getUrl().toString(), is("https://bitbucket.org/tester/test-repo/")); + + instance.withCloneLinks(Arrays.asList( + new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.source(), is("https://bitbucket.org/tester/test-repo")); + + MercurialSCM actual = instance.build(); + assertThat(actual.getCredentialsId(), is(nullValue())); + assertThat(actual.getBrowser(), instanceOf(BitBucket.class)); + assertThat(actual.getBrowser().getUrl().toString(), is("https://bitbucket.org/tester/test-repo/")); + assertThat(actual.getSource(), is("https://bitbucket.org/tester/test-repo")); + assertThat(actual.getRevisionType(), is(MercurialSCM.RevisionType.BRANCH)); + assertThat(actual.getRevision(), is("test-branch")); + } + + @Test + public void given__pullHead_norev_anon__when__build__then__scmBuilt() throws Exception { + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.MERCURIAL), + new SCMHeadOrigin.Fork("qa/qa-repo"), + ChangeRequestCheckoutStrategy.HEAD); + BitbucketHgSCMBuilder instance = new BitbucketHgSCMBuilder(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(instance.cloneLinks(), is(Collections.emptyList())); + assertThat("expecting dummy value until clone links provided or withBitbucketSource called", + instance.source(), is("https://bitbucket.org")); + assertThat(instance.browser(), instanceOf(BitBucket.class)); + assertThat(instance.browser().getUrl().toString(), is("https://bitbucket.org/tester/test-repo/")); + + instance.withCloneLinks(Arrays.asList( + new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.source(), is("https://bitbucket.org/qa/qa-repo")); + + MercurialSCM actual = instance.build(); + assertThat(actual.getCredentialsId(), is(nullValue())); + assertThat(actual.getBrowser(), instanceOf(BitBucket.class)); + assertThat(actual.getBrowser().getUrl().toString(), is("https://bitbucket.org/qa/qa-repo/")); + assertThat(actual.getSource(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getRevisionType(), is(MercurialSCM.RevisionType.BRANCH)); + assertThat(actual.getRevision(), is("qa-branch")); + } + + @Test + public void given__branch_norev_userpass__when__build__then__scmBuilt() throws Exception { + BranchSCMHead head = new BranchSCMHead("test-branch", BitbucketRepositoryType.MERCURIAL); + BitbucketHgSCMBuilder instance = new BitbucketHgSCMBuilder(source, + head, null, "user-pass"); + assertThat(instance.credentialsId(), is("user-pass")); + assertThat(instance.head(), is((SCMHead) head)); + assertThat(instance.revision(), is((nullValue()))); + assertThat(instance.scmSource(), is(source)); + assertThat(instance.cloneLinks(), is(Collections.emptyList())); + assertThat("expecting dummy value until clone links provided or withBitbucketSource called", + instance.source(), is("https://bitbucket.org")); + assertThat(instance.browser(), instanceOf(BitBucket.class)); + assertThat(instance.browser().getUrl().toString(), is("https://bitbucket.org/tester/test-repo/")); + + instance.withCloneLinks(Arrays.asList( + new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.source(), is("https://bitbucket.org/tester/test-repo")); + + MercurialSCM actual = instance.build(); + assertThat(actual.getCredentialsId(), is("user-pass")); + assertThat(actual.getBrowser(), instanceOf(BitBucket.class)); + assertThat(actual.getBrowser().getUrl().toString(), is("https://bitbucket.org/tester/test-repo/")); + assertThat(actual.getSource(), is("https://bitbucket.org/tester/test-repo")); + assertThat(actual.getRevisionType(), is(MercurialSCM.RevisionType.BRANCH)); + assertThat(actual.getRevision(), is("test-branch")); + } + + @Test + public void given__pullHead_norev_userpass__when__build__then__scmBuilt() throws Exception { + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.MERCURIAL), + new SCMHeadOrigin.Fork("qa/qa-repo"), + ChangeRequestCheckoutStrategy.HEAD); + BitbucketHgSCMBuilder instance = new BitbucketHgSCMBuilder(source, + head, null, "user-pass"); + assertThat(instance.credentialsId(), is("user-pass")); + assertThat(instance.head(), is((SCMHead) head)); + assertThat(instance.revision(), is(nullValue())); + assertThat(instance.scmSource(), is(source)); + assertThat(instance.cloneLinks(), is(Collections.emptyList())); + assertThat("expecting dummy value until clone links provided or withBitbucketSource called", + instance.source(), is("https://bitbucket.org")); + assertThat(instance.browser(), instanceOf(BitBucket.class)); + assertThat(instance.browser().getUrl().toString(), is("https://bitbucket.org/tester/test-repo/")); + + instance.withCloneLinks(Arrays.asList( + new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.source(), is("https://bitbucket.org/qa/qa-repo")); + + MercurialSCM actual = instance.build(); + assertThat(actual.getCredentialsId(), is("user-pass")); + assertThat(actual.getBrowser(), instanceOf(BitBucket.class)); + assertThat(actual.getBrowser().getUrl().toString(), is("https://bitbucket.org/qa/qa-repo/")); + assertThat(actual.getSource(), is("https://bitbucket.org/qa/qa-repo")); + assertThat(actual.getRevisionType(), is(MercurialSCM.RevisionType.BRANCH)); + assertThat(actual.getRevision(), is("qa-branch")); + } + + @Test + public void given__branch_norev_userkey__when__build__then__scmBuilt() throws Exception { + BranchSCMHead head = new BranchSCMHead("test-branch", BitbucketRepositoryType.MERCURIAL); + BitbucketHgSCMBuilder instance = new BitbucketHgSCMBuilder(source, + head, null, "user-key"); + assertThat(instance.credentialsId(), is("user-key")); + assertThat(instance.head(), is((SCMHead) head)); + assertThat(instance.revision(), is(nullValue())); + assertThat(instance.scmSource(), is(source)); + assertThat(instance.cloneLinks(), is(Collections.emptyList())); + assertThat("expecting dummy value until clone links provided or withBitbucketSource called", + instance.source(), is("https://bitbucket.org")); + assertThat(instance.browser(), instanceOf(BitBucket.class)); + assertThat(instance.browser().getUrl().toString(), is("https://bitbucket.org/tester/test-repo/")); + + instance.withCloneLinks(Arrays.asList( + new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.source(), is("ssh://hg@bitbucket.org/tester/test-repo")); + + MercurialSCM actual = instance.build(); + assertThat(actual.getCredentialsId(), is("user-key")); + assertThat(actual.getBrowser(), instanceOf(BitBucket.class)); + assertThat(actual.getBrowser().getUrl().toString(), is("https://bitbucket.org/tester/test-repo/")); + assertThat(actual.getSource(), is("ssh://hg@bitbucket.org/tester/test-repo")); + assertThat(actual.getRevisionType(), is(MercurialSCM.RevisionType.BRANCH)); + assertThat(actual.getRevision(), is("test-branch")); + } + + @Test + public void given__pullHead_norev_userkey__when__build__then__scmBuilt() throws Exception { + PullRequestSCMHead head = new PullRequestSCMHead("PR-1", "qa", "qa-repo", "qa-branch", "1", + new BranchSCMHead("test-branch", BitbucketRepositoryType.MERCURIAL), + new SCMHeadOrigin.Fork("qa/qa-repo"), + ChangeRequestCheckoutStrategy.HEAD); + BitbucketHgSCMBuilder instance = new BitbucketHgSCMBuilder(source, + head, null, "user-key"); + assertThat(instance.credentialsId(), is("user-key")); + assertThat(instance.head(), is((SCMHead) head)); + assertThat(instance.revision(), is(nullValue())); + assertThat(instance.scmSource(), is(source)); + assertThat(instance.cloneLinks(), is(Collections.emptyList())); + assertThat("expecting dummy value until clone links provided or withBitbucketSource called", + instance.source(), is("https://bitbucket.org")); + assertThat(instance.browser(), instanceOf(BitBucket.class)); + assertThat(instance.browser().getUrl().toString(), is("https://bitbucket.org/tester/test-repo/")); + + instance.withCloneLinks(Arrays.asList( + new BitbucketHref("https", "https://bitbucket.org/tester/test-repo.git"), + new BitbucketHref("ssh", "ssh://git@bitbucket.org/tester/test-repo.git") + )); + assertThat(instance.source(), is("ssh://hg@bitbucket.org/qa/qa-repo")); + + MercurialSCM actual = instance.build(); + assertThat(actual.getCredentialsId(), is("user-key")); + assertThat(actual.getBrowser(), instanceOf(BitBucket.class)); + assertThat(actual.getBrowser().getUrl().toString(), is("https://bitbucket.org/qa/qa-repo/")); + assertThat(actual.getSource(), is("ssh://hg@bitbucket.org/qa/qa-repo")); + assertThat(actual.getRevisionType(), is(MercurialSCM.RevisionType.BRANCH)); + assertThat(actual.getRevision(), is("qa-branch")); + } + +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest.java new file mode 100644 index 000000000..10b55e211 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest.java @@ -0,0 +1,963 @@ +package com.cloudbees.jenkins.plugins.bitbucket; + +import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint; +import java.util.Arrays; +import java.util.Collections; +import jenkins.model.Jenkins; +import jenkins.scm.api.trait.SCMTrait; +import jenkins.scm.impl.trait.RegexSCMSourceFilterTrait; +import jenkins.scm.impl.trait.WildcardSCMHeadFilterTrait; +import org.hamcrest.Matchers; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +public class BitbucketSCMNavigatorTest { + @ClassRule + public static JenkinsRule j = new JenkinsRule(); + @Rule + public TestName currentTestName = new TestName(); + + private BitbucketSCMNavigator load() { + return load(currentTestName.getMethodName()); + } + + private BitbucketSCMNavigator load(String dataSet) { + return (BitbucketSCMNavigator) Jenkins.XSTREAM2.fromXML( + getClass().getResource(getClass().getSimpleName() + "/" + dataSet + ".xml")); + } + + @Test + public void modern() throws Exception { + BitbucketSCMNavigator instance = load(); + assertThat(instance.id(), is("https://bitbucket.org::cloudbeers")); + assertThat(instance.getRepoOwner(), is("cloudbeers")); + assertThat(instance.getServerUrl(), is(BitbucketCloudEndpoint.SERVER_URL)); + assertThat(instance.getCredentialsId(), is("bcaef157-f105-407f-b150-df7722eab6c1")); + assertThat(instance.getTraits(), is(Collections.>emptyList())); + } + + @Test + public void basic_cloud() throws Exception { + BitbucketSCMNavigator instance = load(); + assertThat(instance.id(), is("https://bitbucket.org::cloudbeers")); + assertThat(instance.getRepoOwner(), is("cloudbeers")); + assertThat(instance.getServerUrl(), is(BitbucketCloudEndpoint.SERVER_URL)); + assertThat(instance.getCredentialsId(), is("bcaef157-f105-407f-b150-df7722eab6c1")); + assertThat("SAME checkout credentials should mean no checkout trait", + instance.getTraits(), + not(hasItem(Matchers.>instanceOf(SSHCheckoutTrait.class)))); + assertThat(".* as a pattern should mean no RegexSCMSourceFilterTrait", + instance.getTraits(), + not(hasItem(Matchers.>instanceOf(RegexSCMSourceFilterTrait.class)))); + assertThat(instance.getTraits(), + containsInAnyOrder( + Matchers.>allOf( + instanceOf(BranchDiscoveryTrait.class), + hasProperty("buildBranch", is(true)), + hasProperty("buildBranchesWithPR", is(true)) + ), + Matchers.>allOf( + instanceOf(OriginPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)) + ), + Matchers.>allOf( + instanceOf(ForkPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)), + hasProperty("trust", instanceOf(ForkPullRequestDiscoveryTrait.TrustEveryone.class)) + ), + Matchers.>instanceOf(PublicRepoPullRequestFilterTrait.class), + Matchers.>allOf( + instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.DISABLE)) + ) + ) + ); + // legacy API + assertThat(instance.getBitbucketServerUrl(), is(nullValue())); + assertThat(instance.getCheckoutCredentialsId(), is("SAME")); + assertThat(instance.getPattern(), is(".*")); + assertThat(instance.isAutoRegisterHooks(), is(false)); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + } + + @Test + public void basic_server() throws Exception { + BitbucketSCMNavigator instance = load(); + assertThat(instance.id(), is("https://bitbucket.test::DUB")); + assertThat(instance.getRepoOwner(), is("DUB")); + assertThat(instance.getServerUrl(), is("https://bitbucket.test")); + assertThat(instance.getCredentialsId(), is("bitbucket")); + assertThat("checkout credentials should mean checkout trait", + instance.getTraits(), + hasItem( + Matchers.>allOf( + Matchers.instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is("8b2e4f77-39c5-41a9-b63b-8d367350bfdf")) + ) + ) + ); + assertThat(".* as a pattern should mean no RegexSCMSourceFilterTrait", + instance.getTraits(), + not(hasItem(Matchers.>instanceOf(RegexSCMSourceFilterTrait.class)))); + assertThat(instance.getTraits(), + containsInAnyOrder( + Matchers.>allOf( + instanceOf(BranchDiscoveryTrait.class), + hasProperty("buildBranch", is(true)), + hasProperty("buildBranchesWithPR", is(true)) + ), + Matchers.>allOf( + instanceOf(OriginPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)) + ), + Matchers.>allOf( + instanceOf(ForkPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)), + hasProperty("trust", instanceOf(ForkPullRequestDiscoveryTrait.TrustEveryone.class)) + ), + Matchers.>instanceOf(PublicRepoPullRequestFilterTrait.class), + Matchers.>allOf( + instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.DISABLE)) + ), + Matchers.>allOf( + Matchers.instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is("8b2e4f77-39c5-41a9-b63b-8d367350bfdf")) + ) + ) + ); + // legacy API + assertThat(instance.getBitbucketServerUrl(), is("https://bitbucket.test")); + assertThat(instance.getCheckoutCredentialsId(), is("8b2e4f77-39c5-41a9-b63b-8d367350bfdf")); + assertThat(instance.getPattern(), is(".*")); + assertThat(instance.isAutoRegisterHooks(), is(false)); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + } + + @Test + public void use_agent_checkout() throws Exception { + BitbucketSCMNavigator instance = load(); + assertThat(instance.id(), is("https://bitbucket.test::DUB")); + assertThat(instance.getRepoOwner(), is("DUB")); + assertThat(instance.getServerUrl(), is("https://bitbucket.test")); + assertThat(instance.getCredentialsId(), is("bitbucket")); + assertThat("checkout credentials should mean checkout trait", + instance.getTraits(), + hasItem( + Matchers.>allOf( + Matchers.instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is(nullValue())) + ) + ) + ); + assertThat(".* as a pattern should mean no RegexSCMSourceFilterTrait", + instance.getTraits(), + not(hasItem(Matchers.>instanceOf(RegexSCMSourceFilterTrait.class)))); + assertThat(instance.getTraits(), + containsInAnyOrder( + Matchers.>allOf( + instanceOf(BranchDiscoveryTrait.class), + hasProperty("buildBranch", is(true)), + hasProperty("buildBranchesWithPR", is(true)) + ), + Matchers.>allOf( + instanceOf(OriginPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)) + ), + Matchers.>allOf( + instanceOf(ForkPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)), + hasProperty("trust", instanceOf(ForkPullRequestDiscoveryTrait.TrustEveryone.class)) + ), + Matchers.>instanceOf(PublicRepoPullRequestFilterTrait.class), + Matchers.>allOf( + instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.DISABLE)) + ), + Matchers.>allOf( + Matchers.instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is(nullValue())) + ) + ) + ); + // legacy API + assertThat(instance.getBitbucketServerUrl(), is("https://bitbucket.test")); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.ANONYMOUS)); + assertThat(instance.getPattern(), is(".*")); + assertThat(instance.isAutoRegisterHooks(), is(false)); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + } + + @Test + public void limit_repositories() throws Exception { + BitbucketSCMNavigator instance = load(); + assertThat(instance.id(), is("https://bitbucket.test::DUB")); + assertThat(instance.getRepoOwner(), is("DUB")); + assertThat(instance.getServerUrl(), is("https://bitbucket.test")); + assertThat(instance.getCredentialsId(), is("bitbucket")); + assertThat("checkout credentials should mean checkout trait", + instance.getTraits(), + hasItem( + Matchers.>allOf( + Matchers.instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is("8b2e4f77-39c5-41a9-b63b-8d367350bfdf")) + ) + ) + ); + assertThat(instance.getTraits(), + containsInAnyOrder( + Matchers.>allOf( + instanceOf(BranchDiscoveryTrait.class), + hasProperty("buildBranch", is(true)), + hasProperty("buildBranchesWithPR", is(true)) + ), + Matchers.>allOf( + instanceOf(OriginPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)) + ), + Matchers.>allOf( + instanceOf(ForkPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)), + hasProperty("trust", instanceOf(ForkPullRequestDiscoveryTrait.TrustEveryone.class)) + ), + Matchers.>instanceOf(PublicRepoPullRequestFilterTrait.class), + Matchers.>allOf( + instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.DISABLE)) + ), + Matchers.>allOf( + Matchers.instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is("8b2e4f77-39c5-41a9-b63b-8d367350bfdf")) + ), + Matchers.>allOf( + instanceOf(RegexSCMSourceFilterTrait.class), + hasProperty("regex", is("limited.*")) + ) + ) + ); + // legacy API + assertThat(instance.getBitbucketServerUrl(), is("https://bitbucket.test")); + assertThat(instance.getCheckoutCredentialsId(), is("8b2e4f77-39c5-41a9-b63b-8d367350bfdf")); + assertThat(instance.getPattern(), is("limited.*")); + assertThat(instance.isAutoRegisterHooks(), is(false)); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + } + + + @Test + public void exclude_branches() throws Exception { + BitbucketSCMNavigator instance = load(); + assertThat(instance.id(), is("https://bitbucket.org::cloudbeers")); + assertThat(instance.getRepoOwner(), is("cloudbeers")); + assertThat(instance.getServerUrl(), is(BitbucketCloudEndpoint.SERVER_URL)); + assertThat(instance.getCredentialsId(), is("bcaef157-f105-407f-b150-df7722eab6c1")); + assertThat(instance.getTraits(), + containsInAnyOrder( + Matchers.>allOf( + instanceOf(BranchDiscoveryTrait.class), + hasProperty("buildBranch", is(true)), + hasProperty("buildBranchesWithPR", is(true)) + ), + Matchers.>allOf( + instanceOf(OriginPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)) + ), + Matchers.>allOf( + instanceOf(ForkPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)), + hasProperty("trust", instanceOf(ForkPullRequestDiscoveryTrait.TrustEveryone.class)) + ), + Matchers.>instanceOf(PublicRepoPullRequestFilterTrait.class), + Matchers.>allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("*")), + hasProperty("excludes", is("master")) + ), + Matchers.>allOf( + instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.DISABLE)) + ) + ) + ); + // legacy API + assertThat(instance.getBitbucketServerUrl(), is(nullValue())); + assertThat(instance.getCheckoutCredentialsId(), is("SAME")); + assertThat(instance.getPattern(), is(".*")); + assertThat(instance.isAutoRegisterHooks(), is(false)); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("master")); + } + + @Test + public void limit_branches() throws Exception { + BitbucketSCMNavigator instance = load(); + assertThat(instance.id(), is("https://bitbucket.org::cloudbeers")); + assertThat(instance.getRepoOwner(), is("cloudbeers")); + assertThat(instance.getServerUrl(), is(BitbucketCloudEndpoint.SERVER_URL)); + assertThat(instance.getCredentialsId(), is("bcaef157-f105-407f-b150-df7722eab6c1")); + assertThat(instance.getTraits(), + containsInAnyOrder( + Matchers.>allOf( + instanceOf(BranchDiscoveryTrait.class), + hasProperty("buildBranch", is(true)), + hasProperty("buildBranchesWithPR", is(true)) + ), + Matchers.>allOf( + instanceOf(OriginPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)) + ), + Matchers.>allOf( + instanceOf(ForkPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)), + hasProperty("trust", instanceOf(ForkPullRequestDiscoveryTrait.TrustEveryone.class)) + ), + Matchers.>instanceOf(PublicRepoPullRequestFilterTrait.class), + Matchers.>allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("feature/*")), + hasProperty("excludes", is("")) + ), + Matchers.>allOf( + instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.DISABLE)) + ) + ) + ); + // legacy API + assertThat(instance.getBitbucketServerUrl(), is(nullValue())); + assertThat(instance.getCheckoutCredentialsId(), is("SAME")); + assertThat(instance.getPattern(), is(".*")); + assertThat(instance.isAutoRegisterHooks(), is(false)); + assertThat(instance.getIncludes(), is("feature/*")); + assertThat(instance.getExcludes(), is("")); + } + + @Test + public void register_hooks() throws Exception { + BitbucketSCMNavigator instance = load(); + assertThat(instance.id(), is("https://bitbucket.test::DUB")); + assertThat(instance.getRepoOwner(), is("DUB")); + assertThat(instance.getServerUrl(), is("https://bitbucket.test")); + assertThat(instance.getCredentialsId(), is("bitbucket")); + assertThat("checkout credentials should mean checkout trait", + instance.getTraits(), + hasItem( + Matchers.>allOf( + Matchers.instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is("8b2e4f77-39c5-41a9-b63b-8d367350bfdf")) + ) + ) + ); + assertThat(".* as a pattern should mean no RegexSCMSourceFilterTrait", + instance.getTraits(), + not(hasItem(Matchers.>instanceOf(RegexSCMSourceFilterTrait.class)))); + assertThat(instance.getTraits(), + containsInAnyOrder( + Matchers.>allOf( + instanceOf(BranchDiscoveryTrait.class), + hasProperty("buildBranch", is(true)), + hasProperty("buildBranchesWithPR", is(true)) + ), + Matchers.>allOf( + instanceOf(OriginPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)) + ), + Matchers.>allOf( + instanceOf(ForkPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)), + hasProperty("trust", instanceOf(ForkPullRequestDiscoveryTrait.TrustEveryone.class)) + ), + Matchers.>instanceOf(PublicRepoPullRequestFilterTrait.class), + Matchers.>allOf( + instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.ITEM)) + ), + Matchers.>allOf( + Matchers.instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is("8b2e4f77-39c5-41a9-b63b-8d367350bfdf")) + ) + ) + ); + // legacy API + assertThat(instance.getBitbucketServerUrl(), is("https://bitbucket.test")); + assertThat(instance.getCheckoutCredentialsId(), is("8b2e4f77-39c5-41a9-b63b-8d367350bfdf")); + assertThat(instance.getPattern(), is(".*")); + assertThat(instance.isAutoRegisterHooks(), is(true)); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + } + + @Test + public void given__instance__when__setTraits_empty__then__traitsEmpty() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Collections.>emptyList()); + assertThat(instance.getTraits(), is(Collections.>emptyList())); + } + + @Test + public void given__instance__when__setTraits__then__traitsSet() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.>asList(new BranchDiscoveryTrait(1), + new WebhookRegistrationTrait(WebhookRegistration.DISABLE))); + assertThat(instance.getTraits(), + containsInAnyOrder( + Matchers.>allOf( + instanceOf(BranchDiscoveryTrait.class), + hasProperty("buildBranch", is(true)), + hasProperty("buildBranchesWithPR", is(false)) + ), + Matchers.>allOf( + instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.DISABLE)) + ) + ) + ); + } + + @Test + public void given__instance__when__setServerUrl__then__urlNormalized() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setServerUrl("https://bitbucket.org:443/foo/../bar/../"); + assertThat(instance.getServerUrl(), is("https://bitbucket.org")); + } + + @Test + public void given__instance__when__setCredentials_empty__then__credentials_null() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setCredentialsId(""); + assertThat(instance.getCredentialsId(), is(nullValue())); + } + + @Test + public void given__instance__when__setCredentials_null__then__credentials_null() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setCredentialsId(""); + assertThat(instance.getCredentialsId(), is(nullValue())); + } + + @Test + public void given__instance__when__setCredentials__then__credentials_set() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setCredentialsId("test"); + assertThat(instance.getCredentialsId(), is("test")); + } + + @Test + public void given__instance__when__setBitbucketServerUrl_null__then__cloudUrlApplied() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setBitbucketServerUrl(null); + assertThat(instance.getServerUrl(), is("https://bitbucket.org")); + assertThat(instance.getBitbucketServerUrl(), is(nullValue())); + } + + @Test + public void given__instance__when__setBitbucketServerUrl_value__then__valueApplied() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setBitbucketServerUrl("https://bitbucket.test"); + assertThat(instance.getServerUrl(), is("https://bitbucket.test")); + assertThat(instance.getBitbucketServerUrl(), is("https://bitbucket.test")); + } + + @Test + public void given__instance__when__setBitbucketServerUrl_value__then__valueNormalized() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setBitbucketServerUrl("https://bitbucket.test/foo/bar/../../"); + assertThat(instance.getServerUrl(), is("https://bitbucket.test")); + assertThat(instance.getBitbucketServerUrl(), is("https://bitbucket.test")); + } + + @Test + public void given__instance__when__setBitbucketServerUrl_cloudUrl__then__valueApplied() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setBitbucketServerUrl("https://bitbucket.org"); + assertThat(instance.getServerUrl(), is("https://bitbucket.org")); + assertThat(instance.getBitbucketServerUrl(), is(nullValue())); + } + + @Test + public void given__legacyCode__when__setPattern_default__then__patternSetAndTraitRemoved() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList(new BranchDiscoveryTrait(true, false), new RegexSCMSourceFilterTrait("job.*"), + new SSHCheckoutTrait("dummy"))); + assertThat(instance.getPattern(), is("job.*")); + assertThat(instance.getTraits(), Matchers.>hasItem(instanceOf(RegexSCMSourceFilterTrait.class))); + instance.setPattern(".*"); + assertThat(instance.getPattern(), is(".*")); + assertThat(instance.getTraits(), + not(Matchers.>hasItem(instanceOf(RegexSCMSourceFilterTrait.class)))); + + } + + @Test + public void given__legacyCode__when__setPattern_custom__then__patternSetAndTraitAdded() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits( + Arrays.>asList(new BranchDiscoveryTrait(true, false), new SSHCheckoutTrait("dummy"))); + assertThat(instance.getPattern(), is(".*")); + assertThat(instance.getTraits(), + not(Matchers.>hasItem(instanceOf(RegexSCMSourceFilterTrait.class)))); + instance.setPattern("job.*"); + assertThat(instance.getPattern(), is("job.*")); + assertThat(instance.getTraits(), Matchers.>hasItem( + allOf(instanceOf(RegexSCMSourceFilterTrait.class), hasProperty("regex", is("job.*"))))); + + } + + @Test + public void given__legacyCode__when__setPattern_custom__then__patternSetAndTraitUpdated() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList(new BranchDiscoveryTrait(true, false), new RegexSCMSourceFilterTrait("job.*"), + new SSHCheckoutTrait("dummy"))); + assertThat(instance.getPattern(), is("job.*")); + assertThat(instance.getTraits(), Matchers.>hasItem(instanceOf(RegexSCMSourceFilterTrait.class))); + instance.setPattern("project.*"); + assertThat(instance.getPattern(), is("project.*")); + assertThat(instance.getTraits(), not(Matchers.>hasItem( + allOf(instanceOf(RegexSCMSourceFilterTrait.class), hasProperty("regex", is("job.*")))))); + assertThat(instance.getTraits(), Matchers.>hasItem( + allOf(instanceOf(RegexSCMSourceFilterTrait.class), hasProperty("regex", is("project.*"))))); + + } + + @Test + public void given__legacyCode__when__setAutoRegisterHooks_true__then__traitAdded() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList(new BranchDiscoveryTrait(true, false), new RegexSCMSourceFilterTrait("job.*"), + new SSHCheckoutTrait("dummy"))); + assertThat(instance.isAutoRegisterHooks(), is(true)); + assertThat(instance.getTraits(), + not(Matchers.>hasItem(instanceOf(WebhookRegistrationTrait.class)))); + instance.setAutoRegisterHooks(true); + assertThat(instance.isAutoRegisterHooks(), is(true)); + assertThat(instance.getTraits(), Matchers.>hasItem( + allOf(instanceOf(WebhookRegistrationTrait.class), hasProperty("mode", is(WebhookRegistration.ITEM))))); + } + + @Test + public void given__legacyCode__when__setAutoRegisterHooks_changes__then__traitUpdated() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList(new BranchDiscoveryTrait(true, false), new RegexSCMSourceFilterTrait("job.*"), + new SSHCheckoutTrait("dummy"))); + assertThat(instance.isAutoRegisterHooks(), is(true)); + assertThat(instance.getTraits(), + not(Matchers.>hasItem(instanceOf(WebhookRegistrationTrait.class)))); + instance.setAutoRegisterHooks(false); + assertThat(instance.isAutoRegisterHooks(), is(false)); + assertThat(instance.getTraits(), Matchers.>hasItem( + allOf(instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.DISABLE))))); + } + + @Test + public void given__legacyCode__when__setAutoRegisterHooks_false__then__traitAdded() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList(new BranchDiscoveryTrait(true, false), new RegexSCMSourceFilterTrait("job.*"), + new SSHCheckoutTrait("dummy"), new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.isAutoRegisterHooks(), is(true)); + assertThat(instance.getTraits(), Matchers.>hasItem( + allOf(instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.SYSTEM))))); + instance.setAutoRegisterHooks(true); + assertThat(instance.isAutoRegisterHooks(), is(true)); + assertThat(instance.getTraits(), Matchers.>hasItem( + allOf(instanceOf(WebhookRegistrationTrait.class), hasProperty("mode", is(WebhookRegistration.ITEM))))); + } + + @Test + public void given__legacyCode__when__setCheckoutCredentials_SAME__then__noTraitAdded() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new RegexSCMSourceFilterTrait("job.*"), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getTraits(), not(Matchers.>hasItem(instanceOf(SSHCheckoutTrait.class)))); + instance.setCheckoutCredentialsId(BitbucketSCMSource.DescriptorImpl.SAME); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getTraits(), not(Matchers.>hasItem(instanceOf(SSHCheckoutTrait.class)))); + } + + @Test + public void given__legacyCode__when__setCheckoutCredentials_SAME__then__traitRemoved() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new RegexSCMSourceFilterTrait("job.*"), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM), + new SSHCheckoutTrait("value"))); + assertThat(instance.getCheckoutCredentialsId(), is("value")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is("value")) + ))); + instance.setCheckoutCredentialsId(BitbucketSCMSource.DescriptorImpl.SAME); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getTraits(), not(Matchers.>hasItem(instanceOf(SSHCheckoutTrait.class)))); + } + + @Test + public void given__legacyCode__when__setCheckoutCredentials_null__then__noTraitAdded() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new RegexSCMSourceFilterTrait("job.*"), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getTraits(), not(Matchers.>hasItem(instanceOf(SSHCheckoutTrait.class)))); + instance.setCheckoutCredentialsId(null); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getTraits(), not(Matchers.>hasItem(instanceOf(SSHCheckoutTrait.class)))); + } + + @Test + public void given__legacyCode__when__setCheckoutCredentials_null__then__traitRemoved() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new RegexSCMSourceFilterTrait("job.*"), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM), + new SSHCheckoutTrait("value"))); + assertThat(instance.getCheckoutCredentialsId(), is("value")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is("value")) + ))); + instance.setCheckoutCredentialsId(null); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getTraits(), not(Matchers.>hasItem(instanceOf(SSHCheckoutTrait.class)))); + } + + @Test + public void given__legacyCode__when__setCheckoutCredentials_value__then__traitAdded() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new RegexSCMSourceFilterTrait("job.*"), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getTraits(), not(Matchers.>hasItem(instanceOf(SSHCheckoutTrait.class)))); + instance.setCheckoutCredentialsId("value"); + assertThat(instance.getCheckoutCredentialsId(), is("value")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is("value")) + ))); + } + + @Test + public void given__legacyCode__when__setCheckoutCredentials_value__then__traitUpdated() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new RegexSCMSourceFilterTrait("job.*"), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM), + new SSHCheckoutTrait(null))); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.ANONYMOUS)); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is(nullValue())) + ))); + instance.setCheckoutCredentialsId("value"); + assertThat(instance.getCheckoutCredentialsId(), is("value")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is("value")) + ))); + } + + @Test + public void given__legacyCode__when__setCheckoutCredentials_ANONYMOUS__then__traitAdded() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new RegexSCMSourceFilterTrait("job.*"), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getTraits(), not(Matchers.>hasItem(instanceOf(SSHCheckoutTrait.class)))); + instance.setCheckoutCredentialsId(BitbucketSCMSource.DescriptorImpl.ANONYMOUS); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.ANONYMOUS)); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is(nullValue())) + ))); + } + + @Test + public void given__legacyCode__when__setCheckoutCredentials_ANONYMOUS__then__traitUpdated() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new RegexSCMSourceFilterTrait("job.*"), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM), + new SSHCheckoutTrait("value"))); + assertThat(instance.getCheckoutCredentialsId(), is("value")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is("value")) + ))); + instance.setCheckoutCredentialsId(BitbucketSCMSource.DescriptorImpl.ANONYMOUS); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.ANONYMOUS)); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is(nullValue())) + ))); + } + + @Test + public void given__legacyCode_withoutExcludes__when__setIncludes_default__then__traitRemoved() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new RegexSCMSourceFilterTrait("job.*"), + new WildcardSCMHeadFilterTrait("feature/*", ""), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getIncludes(), is("feature/*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("feature/*")), + hasProperty("excludes", is("")) + ))); + instance.setIncludes("*"); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getTraits(), not(Matchers.>hasItem( + instanceOf(WildcardSCMHeadFilterTrait.class) + ))); + } + + @Test + public void given__legacyCode_withoutExcludes__when__setIncludes_value__then__traitUpdated() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new RegexSCMSourceFilterTrait("job.*"), + new WildcardSCMHeadFilterTrait("feature/*", ""), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getIncludes(), is("feature/*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("feature/*")), + hasProperty("excludes", is("")) + ))); + instance.setIncludes("bug/*"); + assertThat(instance.getIncludes(), is("bug/*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("bug/*")), + hasProperty("excludes", is("")) + ))); + } + + @Test + public void given__legacyCode_withoutTrait__when__setIncludes_value__then__traitAdded() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new RegexSCMSourceFilterTrait("job.*"), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getTraits(), not(Matchers.>hasItem( + instanceOf(WildcardSCMHeadFilterTrait.class) + ))); + instance.setIncludes("feature/*"); + assertThat(instance.getIncludes(), is("feature/*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("feature/*")), + hasProperty("excludes", is("")) + ))); + } + + @Test + public void given__legacyCode_withExcludes__when__setIncludes_default__then__traitUpdated() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new RegexSCMSourceFilterTrait("job.*"), + new WildcardSCMHeadFilterTrait("feature/*", "feature/ignore"), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getIncludes(), is("feature/*")); + assertThat(instance.getExcludes(), is("feature/ignore")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("feature/*")), + hasProperty("excludes", is("feature/ignore")) + ))); + instance.setIncludes("*"); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("feature/ignore")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("*")), + hasProperty("excludes", is("feature/ignore")) + ))); + } + + @Test + public void given__legacyCode_withExcludes__when__setIncludes_value__then__traitUpdated() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new RegexSCMSourceFilterTrait("job.*"), + new WildcardSCMHeadFilterTrait("feature/*", "feature/ignore"), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getIncludes(), is("feature/*")); + assertThat(instance.getExcludes(), is("feature/ignore")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("feature/*")), + hasProperty("excludes", is("feature/ignore")) + ))); + instance.setIncludes("bug/*"); + assertThat(instance.getIncludes(), is("bug/*")); + assertThat(instance.getExcludes(), is("feature/ignore")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("bug/*")), + hasProperty("excludes", is("feature/ignore")) + ))); + } + + @Test + public void given__legacyCode_withoutIncludes__when__setExcludes_default__then__traitRemoved() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new RegexSCMSourceFilterTrait("job.*"), + new WildcardSCMHeadFilterTrait("*", "feature/ignore"), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("feature/ignore")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("*")), + hasProperty("excludes", is("feature/ignore")) + ))); + instance.setExcludes(""); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getTraits(), not(Matchers.>hasItem( + instanceOf(WildcardSCMHeadFilterTrait.class) + ))); + } + + @Test + public void given__legacyCode_withoutIncludes__when__setExcludes_value__then__traitUpdated() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new RegexSCMSourceFilterTrait("job.*"), + new WildcardSCMHeadFilterTrait("*", "feature/ignore"), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("feature/ignore")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("*")), + hasProperty("excludes", is("feature/ignore")) + ))); + instance.setExcludes("bug/ignore"); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("bug/ignore")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("*")), + hasProperty("excludes", is("bug/ignore")) + ))); + } + + @Test + public void given__legacyCode_withoutTrait__when__setExcludes_value__then__traitAdded() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new RegexSCMSourceFilterTrait("job.*"), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getTraits(), not(Matchers.>hasItem( + instanceOf(WildcardSCMHeadFilterTrait.class) + ))); + instance.setExcludes("feature/ignore"); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("feature/ignore")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("*")), + hasProperty("excludes", is("feature/ignore")) + ))); + } + + @Test + public void given__legacyCode_withIncludes__when__setExcludes_default__then__traitUpdated() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new RegexSCMSourceFilterTrait("job.*"), + new WildcardSCMHeadFilterTrait("feature/*", "feature/ignore"), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getIncludes(), is("feature/*")); + assertThat(instance.getExcludes(), is("feature/ignore")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("feature/*")), + hasProperty("excludes", is("feature/ignore")) + ))); + instance.setExcludes(""); + assertThat(instance.getIncludes(), is("feature/*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("feature/*")), + hasProperty("excludes", is("")) + ))); + } + + @Test + public void given__legacyCode_withIncludes__when__setExcludes_value__then__traitUpdated() { + BitbucketSCMNavigator instance = new BitbucketSCMNavigator("test"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new RegexSCMSourceFilterTrait("job.*"), + new WildcardSCMHeadFilterTrait("feature/*", ""), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getIncludes(), is("feature/*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("feature/*")), + hasProperty("excludes", is("")) + ))); + instance.setExcludes("feature/ignore"); + assertThat(instance.getIncludes(), is("feature/*")); + assertThat(instance.getExcludes(), is("feature/ignore")); + assertThat(instance.getTraits(), Matchers.>hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("feature/*")), + hasProperty("excludes", is("feature/ignore")) + ))); + } + +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest.java new file mode 100644 index 000000000..aa7d0f09a --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest.java @@ -0,0 +1,888 @@ +package com.cloudbees.jenkins.plugins.bitbucket; + +import java.util.Arrays; +import java.util.Collections; +import jenkins.model.Jenkins; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.impl.trait.WildcardSCMHeadFilterTrait; +import org.hamcrest.Matchers; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +public class BitbucketSCMSourceTest { + @ClassRule + public static JenkinsRule j = new JenkinsRule(); + @Rule + public TestName currentTestName = new TestName(); + + private BitbucketSCMSource load() { + return load(currentTestName.getMethodName()); + } + + private BitbucketSCMSource load(String dataSet) { + return (BitbucketSCMSource) Jenkins.XSTREAM2.fromXML( + getClass().getResource(getClass().getSimpleName() + "/" + dataSet + ".xml")); + } + + @Test + public void modern() throws Exception { + BitbucketSCMSource instance = load(); + assertThat(instance.getId(), is("e4d8c11a-0d24-472f-b86b-4b017c160e9a")); + assertThat(instance.getServerUrl(), is("https://bitbucket.org")); + assertThat(instance.getRepoOwner(), is("cloudbeers")); + assertThat(instance.getRepository(), is("stunning-adventure")); + assertThat(instance.getCredentialsId(), is("curl")); + assertThat(instance.getTraits(), is(Collections.emptyList())); + // Legacy API + assertThat(instance.getBitbucketServerUrl(), is(nullValue())); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.isAutoRegisterHook(), is(true)); + } + + @Test + public void basic_cloud_git() throws Exception { + BitbucketSCMSource instance = load(); + assertThat(instance.getId(), is("com.cloudbees.jenkins.plugins.bitbucket" + + ".BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure")); + assertThat(instance.getServerUrl(), is("https://bitbucket.org")); + assertThat(instance.getRepoOwner(), is("cloudbeers")); + assertThat(instance.getRepository(), is("stunning-adventure")); + assertThat(instance.getCredentialsId(), is("bitbucket-cloud")); + assertThat(instance.getTraits(), + containsInAnyOrder( + Matchers.allOf( + instanceOf(BranchDiscoveryTrait.class), + hasProperty("buildBranch", is(true)), + hasProperty("buildBranchesWithPR", is(true)) + ), + Matchers.allOf( + instanceOf(OriginPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)) + ), + Matchers.allOf( + instanceOf(ForkPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)), + hasProperty("trust", instanceOf(ForkPullRequestDiscoveryTrait.TrustEveryone.class)) + ), + Matchers.instanceOf(PublicRepoPullRequestFilterTrait.class), + Matchers.allOf( + instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.DISABLE)) + ) + ) + ); + // Legacy API + assertThat(instance.getBitbucketServerUrl(), is(nullValue())); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.isAutoRegisterHook(), is(false)); + } + + @Test + public void basic_cloud_hg() throws Exception { + BitbucketSCMSource instance = load(); + assertThat(instance.getId(), is("com.cloudbees.jenkins.plugins.bitbucket" + + ".BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::shiny-telegram")); + assertThat(instance.getServerUrl(), is("https://bitbucket.org")); + assertThat(instance.getRepoOwner(), is("cloudbeers")); + assertThat(instance.getRepository(), is("shiny-telegram")); + assertThat(instance.getCredentialsId(), is("bitbucket-cloud")); + assertThat(instance.getTraits(), + containsInAnyOrder( + Matchers.allOf( + instanceOf(BranchDiscoveryTrait.class), + hasProperty("buildBranch", is(true)), + hasProperty("buildBranchesWithPR", is(true)) + ), + Matchers.allOf( + instanceOf(OriginPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)) + ), + Matchers.allOf( + instanceOf(ForkPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)), + hasProperty("trust", instanceOf(ForkPullRequestDiscoveryTrait.TrustEveryone.class)) + ), + Matchers.instanceOf(PublicRepoPullRequestFilterTrait.class), + Matchers.allOf( + instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.DISABLE)) + ) + ) + ); + // Legacy API + assertThat(instance.getBitbucketServerUrl(), is(nullValue())); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.isAutoRegisterHook(), is(false)); + } + + @Test + public void basic_server() throws Exception { + BitbucketSCMSource instance = load(); + assertThat(instance.getId(), is("com.cloudbees.jenkins.plugins.bitbucket" + + ".BitbucketSCMNavigator::https://bitbucket.test::DUB::stunning-adventure")); + assertThat(instance.getServerUrl(), is("https://bitbucket.test")); + assertThat(instance.getRepoOwner(), is("DUB")); + assertThat(instance.getRepository(), is("stunning-adventure")); + assertThat(instance.getCredentialsId(), is("bb-beescloud")); + assertThat(instance.getTraits(), + containsInAnyOrder( + Matchers.allOf( + instanceOf(BranchDiscoveryTrait.class), + hasProperty("buildBranch", is(true)), + hasProperty("buildBranchesWithPR", is(true)) + ), + Matchers.allOf( + instanceOf(OriginPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)) + ), + Matchers.allOf( + instanceOf(ForkPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)), + hasProperty("trust", instanceOf(ForkPullRequestDiscoveryTrait.TrustEveryone.class)) + ), + Matchers.instanceOf(PublicRepoPullRequestFilterTrait.class), + Matchers.allOf( + instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.DISABLE)) + ) + ) + ); + // Legacy API + assertThat(instance.getBitbucketServerUrl(), is("https://bitbucket.test")); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.isAutoRegisterHook(), is(false)); + } + + @Test + public void custom_checkout_credentials() throws Exception { + BitbucketSCMSource instance = load(); + assertThat(instance.getId(), is("com.cloudbees.jenkins.plugins.bitbucket" + + ".BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure")); + assertThat(instance.getServerUrl(), is("https://bitbucket.org")); + assertThat(instance.getRepoOwner(), is("cloudbeers")); + assertThat(instance.getRepository(), is("stunning-adventure")); + assertThat(instance.getCredentialsId(), is("bitbucket-cloud")); + assertThat(instance.getTraits(), + containsInAnyOrder( + Matchers.allOf( + instanceOf(BranchDiscoveryTrait.class), + hasProperty("buildBranch", is(true)), + hasProperty("buildBranchesWithPR", is(true)) + ), + Matchers.allOf( + instanceOf(OriginPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)) + ), + Matchers.allOf( + instanceOf(ForkPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)), + hasProperty("trust", instanceOf(ForkPullRequestDiscoveryTrait.TrustEveryone.class)) + ), + Matchers.instanceOf(PublicRepoPullRequestFilterTrait.class), + Matchers.allOf( + instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.DISABLE)) + ), + Matchers.allOf( + Matchers.instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is("other-credentials")) + ) + ) + ); + // Legacy API + assertThat(instance.getBitbucketServerUrl(), is(nullValue())); + assertThat(instance.getCheckoutCredentialsId(), is("other-credentials")); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.isAutoRegisterHook(), is(false)); + } + + @Test + public void exclude_branches() throws Exception { + BitbucketSCMSource instance = load(); + assertThat(instance.getId(), is("com.cloudbees.jenkins.plugins.bitbucket" + + ".BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure")); + assertThat(instance.getServerUrl(), is("https://bitbucket.org")); + assertThat(instance.getRepoOwner(), is("cloudbeers")); + assertThat(instance.getRepository(), is("stunning-adventure")); + assertThat(instance.getCredentialsId(), is("bitbucket-cloud")); + assertThat(instance.getTraits(), + containsInAnyOrder( + Matchers.allOf( + instanceOf(BranchDiscoveryTrait.class), + hasProperty("buildBranch", is(true)), + hasProperty("buildBranchesWithPR", is(true)) + ), + Matchers.allOf( + instanceOf(OriginPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)) + ), + Matchers.allOf( + instanceOf(ForkPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)), + hasProperty("trust", instanceOf(ForkPullRequestDiscoveryTrait.TrustEveryone.class)) + ), + Matchers.instanceOf(PublicRepoPullRequestFilterTrait.class), + Matchers.allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("*")), + hasProperty("excludes", is("master")) + ), + Matchers.allOf( + instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.DISABLE)) + ) + ) + ); + // Legacy API + assertThat(instance.getBitbucketServerUrl(), is(nullValue())); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("master")); + assertThat(instance.isAutoRegisterHook(), is(false)); + } + + @Test + public void limit_branches() throws Exception { + BitbucketSCMSource instance = load(); + assertThat(instance.getId(), is("com.cloudbees.jenkins.plugins.bitbucket" + + ".BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure")); + assertThat(instance.getServerUrl(), is("https://bitbucket.org")); + assertThat(instance.getRepoOwner(), is("cloudbeers")); + assertThat(instance.getRepository(), is("stunning-adventure")); + assertThat(instance.getCredentialsId(), is("bitbucket-cloud")); + assertThat(instance.getTraits(), + containsInAnyOrder( + Matchers.allOf( + instanceOf(BranchDiscoveryTrait.class), + hasProperty("buildBranch", is(true)), + hasProperty("buildBranchesWithPR", is(true)) + ), + Matchers.allOf( + instanceOf(OriginPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)) + ), + Matchers.allOf( + instanceOf(ForkPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)), + hasProperty("trust", instanceOf(ForkPullRequestDiscoveryTrait.TrustEveryone.class)) + ), + Matchers.instanceOf(PublicRepoPullRequestFilterTrait.class), + Matchers.allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("feature/*")), + hasProperty("excludes", is("")) + ), + Matchers.allOf( + instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.DISABLE)) + ) + ) + ); + // Legacy API + assertThat(instance.getBitbucketServerUrl(), is(nullValue())); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getIncludes(), is("feature/*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.isAutoRegisterHook(), is(false)); + } + + @Test + public void register_hooks() throws Exception { + BitbucketSCMSource instance = load(); + assertThat(instance.getId(), is("com.cloudbees.jenkins.plugins.bitbucket" + + ".BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure")); + assertThat(instance.getServerUrl(), is("https://bitbucket.org")); + assertThat(instance.getRepoOwner(), is("cloudbeers")); + assertThat(instance.getRepository(), is("stunning-adventure")); + assertThat(instance.getCredentialsId(), is("bitbucket-cloud")); + assertThat(instance.getTraits(), + containsInAnyOrder( + Matchers.allOf( + instanceOf(BranchDiscoveryTrait.class), + hasProperty("buildBranch", is(true)), + hasProperty("buildBranchesWithPR", is(true)) + ), + Matchers.allOf( + instanceOf(OriginPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)) + ), + Matchers.allOf( + instanceOf(ForkPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)), + hasProperty("trust", instanceOf(ForkPullRequestDiscoveryTrait.TrustEveryone.class)) + ), + Matchers.instanceOf(PublicRepoPullRequestFilterTrait.class), + Matchers.allOf( + instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.ITEM)) + ) + ) + ); + // Legacy API + assertThat(instance.getBitbucketServerUrl(), is(nullValue())); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.isAutoRegisterHook(), is(true)); + } + + @Test + public void use_agent_checkout() throws Exception { + BitbucketSCMSource instance = load(); + assertThat(instance.getId(), is("com.cloudbees.jenkins.plugins.bitbucket" + + ".BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure")); + assertThat(instance.getServerUrl(), is("https://bitbucket.org")); + assertThat(instance.getRepoOwner(), is("cloudbeers")); + assertThat(instance.getRepository(), is("stunning-adventure")); + assertThat(instance.getCredentialsId(), is("bitbucket-cloud")); + assertThat(instance.getTraits(), + containsInAnyOrder( + Matchers.allOf( + instanceOf(BranchDiscoveryTrait.class), + hasProperty("buildBranch", is(true)), + hasProperty("buildBranchesWithPR", is(true)) + ), + Matchers.allOf( + instanceOf(OriginPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)) + ), + Matchers.allOf( + instanceOf(ForkPullRequestDiscoveryTrait.class), + hasProperty("strategyId", is(2)), + hasProperty("trust", instanceOf(ForkPullRequestDiscoveryTrait.TrustEveryone.class)) + ), + Matchers.instanceOf(PublicRepoPullRequestFilterTrait.class), + Matchers.allOf( + instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.DISABLE)) + ), + Matchers.allOf( + Matchers.instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is(nullValue())) + ) + ) + ); + // Legacy API + assertThat(instance.getBitbucketServerUrl(), is(nullValue())); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.ANONYMOUS)); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.isAutoRegisterHook(), is(false)); + } + + @Test + public void given__instance__when__setTraits_empty__then__traitsEmpty() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Collections.emptyList()); + assertThat(instance.getTraits(), is(Collections.emptyList())); + } + + @Test + public void given__instance__when__setTraits__then__traitsSet() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList(new BranchDiscoveryTrait(1), + new WebhookRegistrationTrait(WebhookRegistration.DISABLE))); + assertThat(instance.getTraits(), + containsInAnyOrder( + Matchers.allOf( + instanceOf(BranchDiscoveryTrait.class), + hasProperty("buildBranch", is(true)), + hasProperty("buildBranchesWithPR", is(false)) + ), + Matchers.allOf( + instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.DISABLE)) + ) + ) + ); + } + + @Test + public void given__instance__when__setServerUrl__then__urlNormalized() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setServerUrl("https://bitbucket.org:443/foo/../bar/../"); + assertThat(instance.getServerUrl(), is("https://bitbucket.org")); + } + + @Test + public void given__instance__when__setCredentials_empty__then__credentials_null() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setCredentialsId(""); + assertThat(instance.getCredentialsId(), is(nullValue())); + } + + @Test + public void given__instance__when__setCredentials_null__then__credentials_null() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setCredentialsId(""); + assertThat(instance.getCredentialsId(), is(nullValue())); + } + + @Test + public void given__instance__when__setCredentials__then__credentials_set() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setCredentialsId("test"); + assertThat(instance.getCredentialsId(), is("test")); + } + + @Test + public void given__instance__when__setBitbucketServerUrl_null__then__cloudUrlApplied() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setBitbucketServerUrl(null); + assertThat(instance.getServerUrl(), is("https://bitbucket.org")); + assertThat(instance.getBitbucketServerUrl(), is(nullValue())); + } + + @Test + public void given__instance__when__setBitbucketServerUrl_value__then__valueApplied() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setBitbucketServerUrl("https://bitbucket.test"); + assertThat(instance.getServerUrl(), is("https://bitbucket.test")); + assertThat(instance.getBitbucketServerUrl(), is("https://bitbucket.test")); + } + + @Test + public void given__instance__when__setBitbucketServerUrl_value__then__valueNormalized() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setBitbucketServerUrl("https://bitbucket.test/foo/bar/../../"); + assertThat(instance.getServerUrl(), is("https://bitbucket.test")); + assertThat(instance.getBitbucketServerUrl(), is("https://bitbucket.test")); + } + + @Test + public void given__instance__when__setBitbucketServerUrl_cloudUrl__then__valueApplied() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setBitbucketServerUrl("https://bitbucket.org"); + assertThat(instance.getServerUrl(), is("https://bitbucket.org")); + assertThat(instance.getBitbucketServerUrl(), is(nullValue())); + } + + @Test + public void given__legacyCode__when__setAutoRegisterHook_true__then__traitAdded() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new SSHCheckoutTrait("dummy"))); + assertThat(instance.isAutoRegisterHook(), is(true)); + assertThat(instance.getTraits(), + not(Matchers.hasItem(instanceOf(WebhookRegistrationTrait.class)))); + instance.setAutoRegisterHook(true); + assertThat(instance.isAutoRegisterHook(), is(true)); + assertThat(instance.getTraits(), Matchers.hasItem( + allOf(instanceOf(WebhookRegistrationTrait.class), hasProperty("mode", is(WebhookRegistration.ITEM))))); + } + + @Test + public void given__legacyCode__when__setAutoRegisterHook_changes__then__traitUpdated() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList(new BranchDiscoveryTrait(true, false), + new SSHCheckoutTrait("dummy"))); + assertThat(instance.isAutoRegisterHook(), is(true)); + assertThat(instance.getTraits(), + not(Matchers.hasItem(instanceOf(WebhookRegistrationTrait.class)))); + instance.setAutoRegisterHook(false); + assertThat(instance.isAutoRegisterHook(), is(false)); + assertThat(instance.getTraits(), Matchers.hasItem( + allOf(instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.DISABLE))))); + } + + @Test + public void given__legacyCode__when__setAutoRegisterHook_false__then__traitAdded() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList(new BranchDiscoveryTrait(true, false), + new SSHCheckoutTrait("dummy"), new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.isAutoRegisterHook(), is(true)); + assertThat(instance.getTraits(), Matchers.hasItem( + allOf(instanceOf(WebhookRegistrationTrait.class), + hasProperty("mode", is(WebhookRegistration.SYSTEM))))); + instance.setAutoRegisterHook(true); + assertThat(instance.isAutoRegisterHook(), is(true)); + assertThat(instance.getTraits(), Matchers.hasItem( + allOf(instanceOf(WebhookRegistrationTrait.class), hasProperty("mode", is(WebhookRegistration.ITEM))))); + } + + @Test + public void given__legacyCode__when__setCheckoutCredentials_SAME__then__noTraitAdded() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getTraits(), not(Matchers.hasItem(instanceOf(SSHCheckoutTrait.class)))); + instance.setCheckoutCredentialsId(BitbucketSCMSource.DescriptorImpl.SAME); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getTraits(), not(Matchers.hasItem(instanceOf(SSHCheckoutTrait.class)))); + } + + @Test + public void given__legacyCode__when__setCheckoutCredentials_SAME__then__traitRemoved() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM), + new SSHCheckoutTrait("value"))); + assertThat(instance.getCheckoutCredentialsId(), is("value")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is("value")) + ))); + instance.setCheckoutCredentialsId(BitbucketSCMSource.DescriptorImpl.SAME); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getTraits(), not(Matchers.hasItem(instanceOf(SSHCheckoutTrait.class)))); + } + + @Test + public void given__legacyCode__when__setCheckoutCredentials_null__then__noTraitAdded() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getTraits(), not(Matchers.hasItem(instanceOf(SSHCheckoutTrait.class)))); + instance.setCheckoutCredentialsId(null); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getTraits(), not(Matchers.hasItem(instanceOf(SSHCheckoutTrait.class)))); + } + + @Test + public void given__legacyCode__when__setCheckoutCredentials_null__then__traitRemoved() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM), + new SSHCheckoutTrait("value"))); + assertThat(instance.getCheckoutCredentialsId(), is("value")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is("value")) + ))); + instance.setCheckoutCredentialsId(null); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getTraits(), not(Matchers.hasItem(instanceOf(SSHCheckoutTrait.class)))); + } + + @Test + public void given__legacyCode__when__setCheckoutCredentials_value__then__traitAdded() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getTraits(), not(Matchers.hasItem(instanceOf(SSHCheckoutTrait.class)))); + instance.setCheckoutCredentialsId("value"); + assertThat(instance.getCheckoutCredentialsId(), is("value")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is("value")) + ))); + } + + @Test + public void given__legacyCode__when__setCheckoutCredentials_value__then__traitUpdated() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM), + new SSHCheckoutTrait(null))); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.ANONYMOUS)); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is(nullValue())) + ))); + instance.setCheckoutCredentialsId("value"); + assertThat(instance.getCheckoutCredentialsId(), is("value")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is("value")) + ))); + } + + @Test + public void given__legacyCode__when__setCheckoutCredentials_ANONYMOUS__then__traitAdded() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.SAME)); + assertThat(instance.getTraits(), not(Matchers.hasItem(instanceOf(SSHCheckoutTrait.class)))); + instance.setCheckoutCredentialsId(BitbucketSCMSource.DescriptorImpl.ANONYMOUS); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.ANONYMOUS)); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is(nullValue())) + ))); + } + + @Test + public void given__legacyCode__when__setCheckoutCredentials_ANONYMOUS__then__traitUpdated() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM), + new SSHCheckoutTrait("value"))); + assertThat(instance.getCheckoutCredentialsId(), is("value")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is("value")) + ))); + instance.setCheckoutCredentialsId(BitbucketSCMSource.DescriptorImpl.ANONYMOUS); + assertThat(instance.getCheckoutCredentialsId(), is(BitbucketSCMSource.DescriptorImpl.ANONYMOUS)); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(SSHCheckoutTrait.class), + hasProperty("credentialsId", is(nullValue())) + ))); + } + + @Test + public void given__legacyCode_withoutExcludes__when__setIncludes_default__then__traitRemoved() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new WildcardSCMHeadFilterTrait("feature/*", ""), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getIncludes(), is("feature/*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("feature/*")), + hasProperty("excludes", is("")) + ))); + instance.setIncludes("*"); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getTraits(), not(Matchers.hasItem( + instanceOf(WildcardSCMHeadFilterTrait.class) + ))); + } + + @Test + public void given__legacyCode_withoutExcludes__when__setIncludes_value__then__traitUpdated() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new WildcardSCMHeadFilterTrait("feature/*", ""), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getIncludes(), is("feature/*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("feature/*")), + hasProperty("excludes", is("")) + ))); + instance.setIncludes("bug/*"); + assertThat(instance.getIncludes(), is("bug/*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("bug/*")), + hasProperty("excludes", is("")) + ))); + } + + @Test + public void given__legacyCode_withoutTrait__when__setIncludes_value__then__traitAdded() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getTraits(), not(Matchers.hasItem( + instanceOf(WildcardSCMHeadFilterTrait.class) + ))); + instance.setIncludes("feature/*"); + assertThat(instance.getIncludes(), is("feature/*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("feature/*")), + hasProperty("excludes", is("")) + ))); + } + + @Test + public void given__legacyCode_withExcludes__when__setIncludes_default__then__traitUpdated() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new WildcardSCMHeadFilterTrait("feature/*", "feature/ignore"), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getIncludes(), is("feature/*")); + assertThat(instance.getExcludes(), is("feature/ignore")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("feature/*")), + hasProperty("excludes", is("feature/ignore")) + ))); + instance.setIncludes("*"); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("feature/ignore")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("*")), + hasProperty("excludes", is("feature/ignore")) + ))); + } + + @Test + public void given__legacyCode_withExcludes__when__setIncludes_value__then__traitUpdated() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new WildcardSCMHeadFilterTrait("feature/*", "feature/ignore"), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getIncludes(), is("feature/*")); + assertThat(instance.getExcludes(), is("feature/ignore")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("feature/*")), + hasProperty("excludes", is("feature/ignore")) + ))); + instance.setIncludes("bug/*"); + assertThat(instance.getIncludes(), is("bug/*")); + assertThat(instance.getExcludes(), is("feature/ignore")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("bug/*")), + hasProperty("excludes", is("feature/ignore")) + ))); + } + + @Test + public void given__legacyCode_withoutIncludes__when__setExcludes_default__then__traitRemoved() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new WildcardSCMHeadFilterTrait("*", "feature/ignore"), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("feature/ignore")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("*")), + hasProperty("excludes", is("feature/ignore")) + ))); + instance.setExcludes(""); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getTraits(), not(Matchers.hasItem( + instanceOf(WildcardSCMHeadFilterTrait.class) + ))); + } + + @Test + public void given__legacyCode_withoutIncludes__when__setExcludes_value__then__traitUpdated() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new WildcardSCMHeadFilterTrait("*", "feature/ignore"), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("feature/ignore")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("*")), + hasProperty("excludes", is("feature/ignore")) + ))); + instance.setExcludes("bug/ignore"); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("bug/ignore")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("*")), + hasProperty("excludes", is("bug/ignore")) + ))); + } + + @Test + public void given__legacyCode_withoutTrait__when__setExcludes_value__then__traitAdded() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getTraits(), not(Matchers.hasItem( + instanceOf(WildcardSCMHeadFilterTrait.class) + ))); + instance.setExcludes("feature/ignore"); + assertThat(instance.getIncludes(), is("*")); + assertThat(instance.getExcludes(), is("feature/ignore")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("*")), + hasProperty("excludes", is("feature/ignore")) + ))); + } + + @Test + public void given__legacyCode_withIncludes__when__setExcludes_default__then__traitUpdated() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new WildcardSCMHeadFilterTrait("feature/*", "feature/ignore"), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getIncludes(), is("feature/*")); + assertThat(instance.getExcludes(), is("feature/ignore")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("feature/*")), + hasProperty("excludes", is("feature/ignore")) + ))); + instance.setExcludes(""); + assertThat(instance.getIncludes(), is("feature/*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("feature/*")), + hasProperty("excludes", is("")) + ))); + } + + @Test + public void given__legacyCode_withIncludes__when__setExcludes_value__then__traitUpdated() { + BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + instance.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, false), + new WildcardSCMHeadFilterTrait("feature/*", ""), + new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); + assertThat(instance.getIncludes(), is("feature/*")); + assertThat(instance.getExcludes(), is("")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("feature/*")), + hasProperty("excludes", is("")) + ))); + instance.setExcludes("feature/ignore"); + assertThat(instance.getIncludes(), is("feature/*")); + assertThat(instance.getExcludes(), is("feature/ignore")); + assertThat(instance.getTraits(), Matchers.hasItem(allOf( + instanceOf(WildcardSCMHeadFilterTrait.class), + hasProperty("includes", is("feature/*")), + hasProperty("excludes", is("feature/ignore")) + ))); + } + +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTraitTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTraitTest.java new file mode 100644 index 000000000..851b8495d --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTraitTest.java @@ -0,0 +1,99 @@ +package com.cloudbees.jenkins.plugins.bitbucket; + +import hudson.util.ListBoxModel; +import java.util.Collections; +import jenkins.scm.api.SCMHeadObserver; +import jenkins.scm.api.trait.SCMHeadFilter; +import jenkins.scm.api.trait.SCMHeadPrefilter; +import org.hamcrest.Matcher; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertThat; +import static org.junit.Assume.assumeThat; + +public class BranchDiscoveryTraitTest { + @ClassRule + public static JenkinsRule j = new JenkinsRule(); + + @Test + public void given__disoverAll__when__appliedToContext__then__noFilter() throws Exception { + BitbucketSCMSourceContext ctx = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()); + assumeThat(ctx.wantBranches(), is(false)); + assumeThat(ctx.wantPRs(), is(false)); + assumeThat(ctx.prefilters(), is(Collections.emptyList())); + assumeThat(ctx.filters(), is(Collections.emptyList())); + assumeThat(ctx.authorities(), not((Matcher) hasItem( + instanceOf(BranchDiscoveryTrait.BranchSCMHeadAuthority.class) + ))); + BranchDiscoveryTrait instance = new BranchDiscoveryTrait(true, true); + instance.decorateContext(ctx); + assertThat(ctx.wantBranches(), is(true)); + assertThat(ctx.wantPRs(), is(false)); + assertThat(ctx.prefilters(), is(Collections.emptyList())); + assertThat(ctx.filters(), is(Collections.emptyList())); + assertThat(ctx.authorities(), (Matcher) hasItem( + instanceOf(BranchDiscoveryTrait.BranchSCMHeadAuthority.class) + )); + } + + @Test + public void given__excludingPRs__when__appliedToContext__then__filter() throws Exception { + BitbucketSCMSourceContext ctx = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()); + assumeThat(ctx.wantBranches(), is(false)); + assumeThat(ctx.wantPRs(), is(false)); + assumeThat(ctx.prefilters(), is(Collections.emptyList())); + assumeThat(ctx.filters(), is(Collections.emptyList())); + assumeThat(ctx.authorities(), not((Matcher) hasItem( + instanceOf(BranchDiscoveryTrait.BranchSCMHeadAuthority.class) + ))); + BranchDiscoveryTrait instance = new BranchDiscoveryTrait(true, false); + instance.decorateContext(ctx); + assertThat(ctx.wantBranches(), is(true)); + assertThat(ctx.wantPRs(), is(true)); + assertThat(ctx.prefilters(), is(Collections.emptyList())); + assertThat(ctx.filters(), + contains(instanceOf(BranchDiscoveryTrait.ExcludeOriginPRBranchesSCMHeadFilter.class))); + assertThat(ctx.authorities(), (Matcher) hasItem( + instanceOf(BranchDiscoveryTrait.BranchSCMHeadAuthority.class) + )); + } + + @Test + public void given__onlyPRs__when__appliedToContext__then__filter() throws Exception { + BitbucketSCMSourceContext ctx = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()); + assumeThat(ctx.wantBranches(), is(false)); + assumeThat(ctx.wantPRs(), is(false)); + assumeThat(ctx.prefilters(), is(Collections.emptyList())); + assumeThat(ctx.filters(), is(Collections.emptyList())); + assumeThat(ctx.authorities(), not((Matcher) hasItem( + instanceOf(BranchDiscoveryTrait.BranchSCMHeadAuthority.class) + ))); + BranchDiscoveryTrait instance = new BranchDiscoveryTrait(false, true); + instance.decorateContext(ctx); + assertThat(ctx.wantBranches(), is(true)); + assertThat(ctx.wantPRs(), is(true)); + assertThat(ctx.prefilters(), is(Collections.emptyList())); + assertThat(ctx.filters(), contains(instanceOf(BranchDiscoveryTrait.OnlyOriginPRBranchesSCMHeadFilter.class))); + assertThat(ctx.authorities(), (Matcher) hasItem( + instanceOf(BranchDiscoveryTrait.BranchSCMHeadAuthority.class) + )); + } + + @Test + public void given__descriptor__when__displayingOptions__then__allThreePresent() { + ListBoxModel options = + j.jenkins.getDescriptorByType(BranchDiscoveryTrait.DescriptorImpl.class).doFillStrategyIdItems(); + assertThat(options.size(), is(3)); + assertThat(options.get(0).value, is("1")); + assertThat(options.get(1).value, is("2")); + assertThat(options.get(2).value, is("3")); + } + +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchScanningIntegrationTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchScanningIntegrationTest.java index 0ff8e3dd7..5a127ff74 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchScanningIntegrationTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchScanningIntegrationTest.java @@ -23,24 +23,9 @@ */ package com.cloudbees.jenkins.plugins.bitbucket; -import static org.hamcrest.Matchers.instanceOf; -import static org.junit.Assert.*; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; - import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryType; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import org.acegisecurity.Authentication; -import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; -import org.junit.Rule; -import org.junit.Test; -import org.jvnet.hudson.test.JenkinsRule; - +import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketEndpointConfiguration; +import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketServerEndpoint; import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.CredentialsScope; @@ -48,7 +33,6 @@ import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.cloudbees.plugins.credentials.domains.Domain; import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; - import hudson.Extension; import hudson.model.FreeStyleBuild; import hudson.model.FreeStyleProject; @@ -58,6 +42,9 @@ import hudson.model.JobPropertyDescriptor; import hudson.model.TaskListener; import hudson.model.TopLevelItem; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import jenkins.branch.Branch; import jenkins.branch.BranchProjectFactory; import jenkins.branch.BranchSource; @@ -66,6 +53,17 @@ import jenkins.branch.MultiBranchProjectDescriptor; import jenkins.scm.api.SCMSource; import jenkins.scm.api.SCMSourceCriteria; +import org.acegisecurity.Authentication; +import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; public class BranchScanningIntegrationTest { @@ -74,12 +72,14 @@ public class BranchScanningIntegrationTest { @Test public void indexingTest() throws Exception { + BitbucketEndpointConfiguration.get() + .addEndpoint(new BitbucketServerEndpoint("test", "http://bitbucket.test", false, null)); BitbucketMockApiFactory.add("http://bitbucket.test", BitbucketClientMockUtils.getAPIClientMock( BitbucketRepositoryType.GIT, false)); MultiBranchProjectImpl p = j.jenkins.createProject(MultiBranchProjectImpl.class, "test"); BitbucketSCMSource source = new BitbucketSCMSource(null, "amuniz", "test-repos"); + source.setServerUrl("http://bitbucket.test"); source.setOwner(p); - source.setBitbucketServerUrl("http://bitbucket.test"); p.getSourcesList().add(new BranchSource(source, new DefaultBranchPropertyStrategy(null))); p.scheduleBuild2(0); j.waitUntilNoActivity(); @@ -93,6 +93,7 @@ public void indexingTest() throws Exception { public void uriResolverByCredentialsTest() throws Exception { WorkflowMultiBranchProject context = j.jenkins.createProject(WorkflowMultiBranchProject.class, "context"); BitbucketSCMSource source = new BitbucketSCMSource(null, "amuniz", "test-repos"); + source.setServerUrl("http://bitbucket.test"); context.getSourcesList().add(new BranchSource(source)); IdCredentials c = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, null, null, "user", "pass"); CredentialsProvider.lookupStores(j.jenkins).iterator().next() 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 490701d36..861bb89e1 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchScanningTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchScanningTest.java @@ -23,37 +23,38 @@ */ package com.cloudbees.jenkins.plugins.bitbucket; -import static org.junit.Assert.*; -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryType; +import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudApiClient; +import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint; +import edu.umd.cs.findbugs.annotations.NonNull; import hudson.model.TaskListener; import hudson.plugins.git.GitSCM; -import hudson.plugins.git.UserRemoteConfig; import hudson.plugins.mercurial.MercurialSCM; import hudson.scm.SCM; - import java.io.IOException; import java.util.ArrayList; import java.util.List; - import jenkins.plugins.git.AbstractGitSCMSource.SCMRevisionImpl; import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMHeadObserver; import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMSource; import jenkins.scm.api.SCMSourceCriteria; import jenkins.scm.api.SCMSourceOwner; -import jenkins.scm.api.SCMSource; - +import org.hamcrest.Matchers; +import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; - -import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudApiClient; - -import edu.umd.cs.findbugs.annotations.NonNull; import org.jvnet.hudson.test.JenkinsRule; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + public class BranchScanningTest { @ClassRule @@ -63,31 +64,38 @@ public class BranchScanningTest { private static final String repoName = "test"; private static final String branchName = "branch1"; + @Before + public void clearMockFactory() { + BitbucketMockApiFactory.clear(); + } + @Test public void uriResolverTest() throws Exception { - BitbucketSCMSource source = getBitbucketSCMSourceMock(BitbucketRepositoryType.GIT); - String remote = source.getRemote("amuniz", "test", source.getRepositoryType()); // When there is no checkout credentials set, https must be resolved - assertEquals("https://bitbucket.org/amuniz/test.git", remote); - - source = getBitbucketSCMSourceMock(BitbucketRepositoryType.MERCURIAL); - remote = source.getRemote("amuniz", "test", source.getRepositoryType()); + assertThat(new BitbucketGitSCMBuilder(getBitbucketSCMSourceMock(BitbucketRepositoryType.GIT), + new BranchSCMHead("branch1", BitbucketRepositoryType.GIT), null, + null).withBitbucketRemote().remote(), is("https://bitbucket.org/amuniz/test-repos.git")); // Resolve URL for Mercurial repositories - assertEquals("https://bitbucket.org/amuniz/test", remote); + assertThat(new BitbucketHgSCMBuilder(getBitbucketSCMSourceMock(BitbucketRepositoryType.MERCURIAL), + new BranchSCMHead("branch1", BitbucketRepositoryType.MERCURIAL), null, + null).withBitbucketSource().source(), is("https://bitbucket.org/amuniz/test-repos")); } @Test public void remoteConfigsTest() throws Exception { BitbucketSCMSource source = getBitbucketSCMSourceMock(BitbucketRepositoryType.GIT); - List remoteConfigs = source.getGitRemoteConfigs(new BranchSCMHead("branch1", BitbucketRepositoryType.GIT)); - assertEquals(1, remoteConfigs.size()); - assertEquals("+refs/heads/branch1", remoteConfigs.get(0).getRefspec()); + BitbucketGitSCMBuilder builder = + new BitbucketGitSCMBuilder(source, new BranchSCMHead("branch1", BitbucketRepositoryType.GIT), null, + null); + assertThat(builder.refSpecs(), Matchers.contains("+refs/heads/branch1:refs/remotes/@{remote}/branch1")); } @Test public void retrieveTest() throws Exception { + BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, + BitbucketClientMockUtils.getAPIClientMock(BitbucketRepositoryType.GIT, false)); BitbucketSCMSource source = getBitbucketSCMSourceMock(BitbucketRepositoryType.GIT); BranchSCMHead head = new BranchSCMHead(branchName, BitbucketRepositoryType.GIT); @@ -100,6 +108,8 @@ public void retrieveTest() throws Exception { @Test public void scanTest() throws Exception { + BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, + BitbucketClientMockUtils.getAPIClientMock(BitbucketRepositoryType.GIT, false)); BitbucketSCMSource source = getBitbucketSCMSourceMock(BitbucketRepositoryType.GIT); SCMHeadObserverImpl observer = new SCMHeadObserverImpl(); source.fetch(observer, BitbucketClientMockUtils.getTaskListenerMock()); @@ -111,6 +121,8 @@ public void scanTest() throws Exception { @Test public void scanTestPullRequests() throws Exception { + BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, + BitbucketClientMockUtils.getAPIClientMock(BitbucketRepositoryType.GIT, true)); BitbucketSCMSource source = getBitbucketSCMSourceMock(BitbucketRepositoryType.GIT, true); SCMHeadObserverImpl observer = new SCMHeadObserverImpl(); source.fetch(observer, BitbucketClientMockUtils.getTaskListenerMock()); @@ -123,12 +135,16 @@ public void scanTestPullRequests() throws Exception { @Test public void gitSCMTest() throws Exception { + BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, + BitbucketClientMockUtils.getAPIClientMock(BitbucketRepositoryType.GIT, false)); SCM scm = scmBuild(BitbucketRepositoryType.GIT); assertTrue("SCM must be an instance of GitSCM", scm instanceof GitSCM); } @Test public void mercurialSCMTest() throws Exception { + BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, + BitbucketClientMockUtils.getAPIClientMock(BitbucketRepositoryType.MERCURIAL, false)); SCM scm = scmBuild(BitbucketRepositoryType.MERCURIAL); assertTrue("SCM must be an instance of MercurialSCM", scm instanceof MercurialSCM); } @@ -141,7 +157,8 @@ private SCM scmBuild(BitbucketRepositoryType type) throws IOException, Interrupt private BitbucketSCMSource getBitbucketSCMSourceMock(BitbucketRepositoryType type, boolean includePullRequests) throws IOException, InterruptedException { BitbucketCloudApiClient mock = BitbucketClientMockUtils.getAPIClientMock(type, includePullRequests); - BitbucketMockApiFactory.add(null, mock); + + BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, mock); BitbucketSCMSource source = new BitbucketSCMSource(null, "amuniz", "test-repos"); source.setOwner(getSCMSourceOwnerMock()); diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTraitTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTraitTest.java new file mode 100644 index 000000000..5417b501c --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTraitTest.java @@ -0,0 +1,125 @@ +package com.cloudbees.jenkins.plugins.bitbucket; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; +import jenkins.scm.api.SCMHeadObserver; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; +import jenkins.scm.api.trait.SCMHeadFilter; +import jenkins.scm.api.trait.SCMHeadPrefilter; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; +import org.junit.Test; + +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertThat; +import static org.junit.Assume.assumeThat; + +public class ForkPullRequestDiscoveryTraitTest { + @Test + public void given__disoverHeadMerge__when__appliedToContext__then__strategiesCorrect() throws Exception { + BitbucketSCMSourceContext ctx = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()); + assumeThat(ctx.wantBranches(), is(false)); + assumeThat(ctx.wantPRs(), is(false)); + assumeThat(ctx.prefilters(), is(Collections.emptyList())); + assumeThat(ctx.filters(), is(Collections.emptyList())); + assumeThat(ctx.authorities(), not((Matcher) hasItem( + instanceOf(ForkPullRequestDiscoveryTrait.TrustTeamForks.class) + ))); + ForkPullRequestDiscoveryTrait instance = new ForkPullRequestDiscoveryTrait( + EnumSet.allOf(ChangeRequestCheckoutStrategy.class), + new ForkPullRequestDiscoveryTrait.TrustTeamForks() + ); + instance.decorateContext(ctx); + assertThat(ctx.wantBranches(), is(false)); + assertThat(ctx.wantPRs(), is(true)); + assertThat(ctx.prefilters(), is(Collections.emptyList())); + assertThat(ctx.filters(), is(Collections.emptyList())); + assertThat(ctx.forkPRStrategies(), + Matchers.>is(EnumSet.allOf(ChangeRequestCheckoutStrategy.class))); + assertThat(ctx.authorities(), (Matcher) hasItem( + instanceOf(ForkPullRequestDiscoveryTrait.TrustTeamForks.class) + )); + } + + @Test + public void given__disoverHeadOnly__when__appliedToContext__then__strategiesCorrect() throws Exception { + BitbucketSCMSourceContext ctx = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()); + assumeThat(ctx.wantBranches(), is(false)); + assumeThat(ctx.wantPRs(), is(false)); + assumeThat(ctx.prefilters(), is(Collections.emptyList())); + assumeThat(ctx.filters(), is(Collections.emptyList())); + assumeThat(ctx.authorities(), not((Matcher) hasItem( + instanceOf(ForkPullRequestDiscoveryTrait.TrustTeamForks.class) + ))); + ForkPullRequestDiscoveryTrait instance = new ForkPullRequestDiscoveryTrait( + EnumSet.of(ChangeRequestCheckoutStrategy.HEAD), + new ForkPullRequestDiscoveryTrait.TrustTeamForks() + ); + instance.decorateContext(ctx); + assertThat(ctx.wantBranches(), is(false)); + assertThat(ctx.wantPRs(), is(true)); + assertThat(ctx.prefilters(), is(Collections.emptyList())); + assertThat(ctx.filters(), is(Collections.emptyList())); + assertThat(ctx.forkPRStrategies(), + Matchers.>is(EnumSet.of(ChangeRequestCheckoutStrategy.HEAD))); + assertThat(ctx.authorities(), (Matcher) hasItem( + instanceOf(ForkPullRequestDiscoveryTrait.TrustTeamForks.class) + )); + } + + @Test + public void given__disoverMergeOnly__when__appliedToContext__then__strategiesCorrect() throws Exception { + BitbucketSCMSourceContext ctx = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()); + assumeThat(ctx.wantBranches(), is(false)); + assumeThat(ctx.wantPRs(), is(false)); + assumeThat(ctx.prefilters(), is(Collections.emptyList())); + assumeThat(ctx.filters(), is(Collections.emptyList())); + assumeThat(ctx.authorities(), not((Matcher) hasItem( + instanceOf(ForkPullRequestDiscoveryTrait.TrustTeamForks.class) + ))); + ForkPullRequestDiscoveryTrait instance = new ForkPullRequestDiscoveryTrait( + EnumSet.of(ChangeRequestCheckoutStrategy.MERGE), + new ForkPullRequestDiscoveryTrait.TrustTeamForks() + ); + instance.decorateContext(ctx); + assertThat(ctx.wantBranches(), is(false)); + assertThat(ctx.wantPRs(), is(true)); + assertThat(ctx.prefilters(), is(Collections.emptyList())); + assertThat(ctx.filters(), is(Collections.emptyList())); + assertThat(ctx.forkPRStrategies(), + Matchers.>is(EnumSet.of(ChangeRequestCheckoutStrategy.MERGE))); + assertThat(ctx.authorities(), (Matcher) hasItem( + instanceOf(ForkPullRequestDiscoveryTrait.TrustTeamForks.class) + )); + } + + @Test + public void given__nonDefaultTrust__when__appliedToContext__then__authoritiesCorrect() throws Exception { + BitbucketSCMSourceContext ctx = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()); + assumeThat(ctx.wantBranches(), is(false)); + assumeThat(ctx.wantPRs(), is(false)); + assumeThat(ctx.prefilters(), is(Collections.emptyList())); + assumeThat(ctx.filters(), is(Collections.emptyList())); + assumeThat(ctx.authorities(), not((Matcher) hasItem( + instanceOf(ForkPullRequestDiscoveryTrait.TrustTeamForks.class) + ))); + ForkPullRequestDiscoveryTrait instance = new ForkPullRequestDiscoveryTrait( + EnumSet.allOf(ChangeRequestCheckoutStrategy.class), + new ForkPullRequestDiscoveryTrait.TrustEveryone() + ); + instance.decorateContext(ctx); + assertThat(ctx.wantBranches(), is(false)); + assertThat(ctx.wantPRs(), is(true)); + assertThat(ctx.prefilters(), is(Collections.emptyList())); + assertThat(ctx.filters(), is(Collections.emptyList())); + assertThat(ctx.forkPRStrategies(), + Matchers.>is(EnumSet.allOf(ChangeRequestCheckoutStrategy.class))); + assertThat(ctx.authorities(), (Matcher) hasItem( + instanceOf(ForkPullRequestDiscoveryTrait.TrustEveryone.class) + )); + } +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTraitTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTraitTest.java new file mode 100644 index 000000000..91efcc60f --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTraitTest.java @@ -0,0 +1,120 @@ +package com.cloudbees.jenkins.plugins.bitbucket; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; +import jenkins.scm.api.SCMHeadObserver; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; +import jenkins.scm.api.trait.SCMHeadFilter; +import jenkins.scm.api.trait.SCMHeadPrefilter; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; +import org.junit.Test; + +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertThat; +import static org.junit.Assume.assumeThat; + +public class OriginPullRequestDiscoveryTraitTest { + @Test + public void given__disoverHeadMerge__when__appliedToContext__then__strategiesCorrect() throws Exception { + BitbucketSCMSourceContext ctx = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()); + assumeThat(ctx.wantBranches(), is(false)); + assumeThat(ctx.wantPRs(), is(false)); + assumeThat(ctx.prefilters(), is(Collections.emptyList())); + assumeThat(ctx.filters(), is(Collections.emptyList())); + assumeThat(ctx.authorities(), not((Matcher) hasItem( + instanceOf(OriginPullRequestDiscoveryTrait.OriginChangeRequestSCMHeadAuthority.class) + ))); + OriginPullRequestDiscoveryTrait instance = new OriginPullRequestDiscoveryTrait( + EnumSet.allOf(ChangeRequestCheckoutStrategy.class) + ); + instance.decorateContext(ctx); + assertThat(ctx.wantBranches(), is(false)); + assertThat(ctx.wantPRs(), is(true)); + assertThat(ctx.prefilters(), is(Collections.emptyList())); + assertThat(ctx.filters(), is(Collections.emptyList())); + assertThat(ctx.originPRStrategies(), + Matchers.>is(EnumSet.allOf(ChangeRequestCheckoutStrategy.class))); + assertThat(ctx.authorities(), (Matcher) hasItem( + instanceOf(OriginPullRequestDiscoveryTrait.OriginChangeRequestSCMHeadAuthority.class) + )); + } + + @Test + public void given__disoverHeadOnly__when__appliedToContext__then__strategiesCorrect() throws Exception { + BitbucketSCMSourceContext ctx = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()); + assumeThat(ctx.wantBranches(), is(false)); + assumeThat(ctx.wantPRs(), is(false)); + assumeThat(ctx.prefilters(), is(Collections.emptyList())); + assumeThat(ctx.filters(), is(Collections.emptyList())); + assumeThat(ctx.authorities(), not((Matcher) hasItem( + instanceOf(OriginPullRequestDiscoveryTrait.OriginChangeRequestSCMHeadAuthority.class) + ))); + OriginPullRequestDiscoveryTrait instance = new OriginPullRequestDiscoveryTrait( + EnumSet.of(ChangeRequestCheckoutStrategy.HEAD) + ); + instance.decorateContext(ctx); + assertThat(ctx.wantBranches(), is(false)); + assertThat(ctx.wantPRs(), is(true)); + assertThat(ctx.prefilters(), is(Collections.emptyList())); + assertThat(ctx.filters(), is(Collections.emptyList())); + assertThat(ctx.originPRStrategies(), + Matchers.>is(EnumSet.of(ChangeRequestCheckoutStrategy.HEAD))); + assertThat(ctx.authorities(), (Matcher) hasItem( + instanceOf(OriginPullRequestDiscoveryTrait.OriginChangeRequestSCMHeadAuthority.class) + )); + } + + @Test + public void given__disoverMergeOnly__when__appliedToContext__then__strategiesCorrect() throws Exception { + BitbucketSCMSourceContext ctx = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()); + assumeThat(ctx.wantBranches(), is(false)); + assumeThat(ctx.wantPRs(), is(false)); + assumeThat(ctx.prefilters(), is(Collections.emptyList())); + assumeThat(ctx.filters(), is(Collections.emptyList())); + assumeThat(ctx.authorities(), not((Matcher) hasItem( + instanceOf(OriginPullRequestDiscoveryTrait.OriginChangeRequestSCMHeadAuthority.class) + ))); + OriginPullRequestDiscoveryTrait instance = new OriginPullRequestDiscoveryTrait( + EnumSet.of(ChangeRequestCheckoutStrategy.MERGE) + ); + instance.decorateContext(ctx); + assertThat(ctx.wantBranches(), is(false)); + assertThat(ctx.wantPRs(), is(true)); + assertThat(ctx.prefilters(), is(Collections.emptyList())); + assertThat(ctx.filters(), is(Collections.emptyList())); + assertThat(ctx.originPRStrategies(), + Matchers.>is(EnumSet.of(ChangeRequestCheckoutStrategy.MERGE))); + assertThat(ctx.authorities(), (Matcher) hasItem( + instanceOf(OriginPullRequestDiscoveryTrait.OriginChangeRequestSCMHeadAuthority.class) + )); + } + + @Test + public void given__programmaticConstructor__when__appliedToContext__then__strategiesCorrect() throws Exception { + BitbucketSCMSourceContext ctx = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()); + assumeThat(ctx.wantBranches(), is(false)); + assumeThat(ctx.wantPRs(), is(false)); + assumeThat(ctx.prefilters(), is(Collections.emptyList())); + assumeThat(ctx.filters(), is(Collections.emptyList())); + assumeThat(ctx.authorities(), not((Matcher) hasItem( + instanceOf(OriginPullRequestDiscoveryTrait.OriginChangeRequestSCMHeadAuthority.class) + ))); + OriginPullRequestDiscoveryTrait instance = + new OriginPullRequestDiscoveryTrait(EnumSet.allOf(ChangeRequestCheckoutStrategy.class)); + instance.decorateContext(ctx); + assertThat(ctx.wantBranches(), is(false)); + assertThat(ctx.wantPRs(), is(true)); + assertThat(ctx.prefilters(), is(Collections.emptyList())); + assertThat(ctx.filters(), is(Collections.emptyList())); + assertThat(ctx.originPRStrategies(), + Matchers.>is(EnumSet.allOf(ChangeRequestCheckoutStrategy.class))); + assertThat(ctx.authorities(), (Matcher) hasItem( + instanceOf(OriginPullRequestDiscoveryTrait.OriginChangeRequestSCMHeadAuthority.class) + )); + } +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTraitTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTraitTest.java new file mode 100644 index 000000000..bc232cc80 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTraitTest.java @@ -0,0 +1,24 @@ +package com.cloudbees.jenkins.plugins.bitbucket; + +import jenkins.scm.api.SCMHeadObserver; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assume.assumeThat; + +public class PublicRepoPullRequestFilterTraitTest { + @ClassRule + public static JenkinsRule j = new JenkinsRule(); + + @Test + public void given__instance__when__decoratingContext__then__filterApplied() throws Exception { + PublicRepoPullRequestFilterTrait instance = new PublicRepoPullRequestFilterTrait(); + BitbucketSCMSourceContext probe = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()); + assumeThat(probe.skipPublicPRs(), is(false)); + instance.decorateContext(probe); + assertThat(probe.skipPublicPRs(), is(true)); + } +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/SCMNavigatorIntegrationTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/SCMNavigatorIntegrationTest.java index 0da01b9f9..effac3710 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/SCMNavigatorIntegrationTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/SCMNavigatorIntegrationTest.java @@ -24,23 +24,23 @@ package com.cloudbees.jenkins.plugins.bitbucket; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryType; -import java.util.Map; - -import static org.junit.Assert.*; - -import org.junit.Rule; -import org.junit.Test; -import org.jvnet.hudson.test.JenkinsRule; -import org.jvnet.hudson.test.TestExtension; -import org.kohsuke.stapler.DataBoundConstructor; - +import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketEndpointConfiguration; +import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketServerEndpoint; import hudson.model.ItemGroup; +import java.util.Map; import jenkins.branch.MultiBranchProject; import jenkins.branch.MultiBranchProjectFactory; import jenkins.branch.MultiBranchProjectFactoryDescriptor; import jenkins.branch.OrganizationFolder; import jenkins.scm.api.SCMSource; import jenkins.scm.api.SCMSourceCriteria; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestExtension; +import org.kohsuke.stapler.DataBoundConstructor; + +import static org.junit.Assert.assertEquals; public class SCMNavigatorIntegrationTest { @@ -49,6 +49,8 @@ public class SCMNavigatorIntegrationTest { @Test public void teamDiscoveringTest() throws Exception { + BitbucketEndpointConfiguration + .get().addEndpoint(new BitbucketServerEndpoint("test", "http://bitbucket.test", false, null)); BitbucketMockApiFactory.add("http://bitbucket.test", BitbucketClientMockUtils.getAPIClientMock(BitbucketRepositoryType.GIT, true)); OrganizationFolder teamFolder = j.jenkins.createProject(OrganizationFolder.class, "test"); diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTraitTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTraitTest.java new file mode 100644 index 000000000..bab72e674 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTraitTest.java @@ -0,0 +1,137 @@ +package com.cloudbees.jenkins.plugins.bitbucket; + +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryType; +import hudson.model.Item; +import hudson.model.User; +import hudson.security.ACL; +import hudson.security.AuthorizationStrategy; +import hudson.security.SecurityRealm; +import hudson.util.ListBoxModel; +import jenkins.model.Jenkins; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.MockAuthorizationStrategy; +import org.jvnet.hudson.test.MockFolder; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assume.assumeThat; + +public class SSHCheckoutTraitTest { + @ClassRule + public static JenkinsRule j = new JenkinsRule(); + + @Test + public void given__legacyConfig__when__creatingTrait__then__convertedToModern() throws Exception { + assertThat(new SSHCheckoutTrait(BitbucketSCMSource.DescriptorImpl.ANONYMOUS).getCredentialsId(), + is(nullValue())); + } + + @Test + public void given__sshCheckoutWithCredentials__when__decoratingGit__then__credentialsApplied() throws Exception { + SSHCheckoutTrait instance = new SSHCheckoutTrait("keyId"); + BitbucketGitSCMBuilder probe = + new BitbucketGitSCMBuilder(new BitbucketSCMSource(null, "example", "does-not-exist"), + new BranchSCMHead("master", BitbucketRepositoryType.GIT), null, "scanId"); + assumeThat(probe.credentialsId(), is("scanId")); + instance.decorateBuilder(probe); + assertThat(probe.credentialsId(), is("keyId")); + } + + @Test + public void given__sshCheckoutWithAgentKey__when__decoratingGit__then__useAgentKeyApplied() throws Exception { + SSHCheckoutTrait instance = new SSHCheckoutTrait(null); + BitbucketGitSCMBuilder probe = + new BitbucketGitSCMBuilder(new BitbucketSCMSource(null, "example", "does-not-exist"), + new BranchSCMHead("master", BitbucketRepositoryType.GIT), null, "scanId"); + assumeThat(probe.credentialsId(), is("scanId")); + instance.decorateBuilder(probe); + assertThat(probe.credentialsId(), is(nullValue())); + } + + @Test + public void given__sshCheckoutWithCredentials__when__decoratingHg__then__credentialsApplied() throws Exception { + SSHCheckoutTrait instance = new SSHCheckoutTrait("keyId"); + BitbucketHgSCMBuilder probe = + new BitbucketHgSCMBuilder(new BitbucketSCMSource(null, "example", "does-not-exist"), + new BranchSCMHead("master", BitbucketRepositoryType.MERCURIAL), null, "scanId"); + assumeThat(probe.credentialsId(), is("scanId")); + instance.decorateBuilder(probe); + assertThat(probe.credentialsId(), is("keyId")); + } + + @Test + public void given__sshCheckoutWithAgentKey__when__decoratingHg__then__useAgentKeyApplied() throws Exception { + SSHCheckoutTrait instance = new SSHCheckoutTrait(null); + BitbucketHgSCMBuilder probe = + new BitbucketHgSCMBuilder(new BitbucketSCMSource(null, "example", "does-not-exist"), + new BranchSCMHead("master", BitbucketRepositoryType.MERCURIAL), null, "scanId"); + assumeThat(probe.credentialsId(), is("scanId")); + instance.decorateBuilder(probe); + assertThat(probe.credentialsId(), is(nullValue())); + } + + @Test + public void given__descriptor__when__displayingCredentials__then__contractEnforced() throws Exception { + final SSHCheckoutTrait.DescriptorImpl d = j.jenkins.getDescriptorByType(SSHCheckoutTrait.DescriptorImpl.class); + final MockFolder dummy = j.createFolder("dummy"); + SecurityRealm realm = j.jenkins.getSecurityRealm(); + AuthorizationStrategy strategy = j.jenkins.getAuthorizationStrategy(); + try { + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + MockAuthorizationStrategy mockStrategy = new MockAuthorizationStrategy(); + mockStrategy.grant(Jenkins.ADMINISTER).onRoot().to("admin"); + mockStrategy.grant(Item.CONFIGURE).onItems(dummy).to("bob"); + mockStrategy.grant(Item.EXTENDED_READ).onItems(dummy).to("jim"); + j.jenkins.setAuthorizationStrategy(mockStrategy); + ACL.impersonate(User.get("admin").impersonate(), new Runnable() { + @Override + public void run() { + ListBoxModel rsp = d.doFillCredentialsIdItems(dummy, "", "does-not-exist"); + assertThat("Expecting only the provided value so that form config unchanged", rsp, hasSize(1)); + assertThat("Expecting only the provided value so that form config unchanged", rsp.get(0).value, + is("does-not-exist")); + rsp = d.doFillCredentialsIdItems(null, "", "does-not-exist"); + assertThat("Expecting just the empty entry", rsp, hasSize(1)); + assertThat("Expecting just the empty entry", rsp.get(0).value, is("")); + } + }); + ACL.impersonate(User.get("bob").impersonate(), new Runnable() { + @Override + public void run() { + ListBoxModel rsp = d.doFillCredentialsIdItems(dummy, "", "does-not-exist"); + assertThat("Expecting just the empty entry", rsp, hasSize(1)); + assertThat("Expecting just the empty entry", rsp.get(0).value, is("")); + rsp = d.doFillCredentialsIdItems(null, "", "does-not-exist"); + assertThat("Expecting only the provided value so that form config unchanged", rsp, hasSize(1)); + assertThat("Expecting only the provided value so that form config unchanged", rsp.get(0).value, + is("does-not-exist")); + } + }); + ACL.impersonate(User.get("jim").impersonate(), new Runnable() { + @Override + public void run() { + ListBoxModel rsp = d.doFillCredentialsIdItems(dummy, "", "does-not-exist"); + assertThat("Expecting just the empty entry", rsp, hasSize(1)); + assertThat("Expecting just the empty entry", rsp.get(0).value, is("")); + } + }); + ACL.impersonate(User.get("sue").impersonate(), new Runnable() { + @Override + public void run() { + ListBoxModel rsp = d.doFillCredentialsIdItems(dummy, "", "does-not-exist"); + assertThat("Expecting only the provided value so that form config unchanged", rsp, hasSize(1)); + assertThat("Expecting only the provided value so that form config unchanged", rsp.get(0).value, + is("does-not-exist")); + } + }); + } finally { + j.jenkins.setSecurityRealm(realm); + j.jenkins.setAuthorizationStrategy(strategy); + j.jenkins.remove(dummy); + } + } +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTraitTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTraitTest.java new file mode 100644 index 000000000..fd9438e0b --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTraitTest.java @@ -0,0 +1,47 @@ +package com.cloudbees.jenkins.plugins.bitbucket; + +import hudson.util.ListBoxModel; +import jenkins.scm.api.SCMHeadObserver; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertThat; +import static org.junit.Assume.assumeThat; + +public class WebhookRegistrationTraitTest { + @ClassRule + public static JenkinsRule j = new JenkinsRule(); + + @Test + public void given__webhookRegistrationDisabled__when__appliedToContext__then__webhookRegistrationDisabled() + throws Exception { + BitbucketSCMSourceContext ctx = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()); + assumeThat(ctx.webhookRegistration(), is(WebhookRegistration.SYSTEM)); + WebhookRegistrationTrait instance = new WebhookRegistrationTrait(WebhookRegistration.DISABLE.toString()); + instance.decorateContext(ctx); + assertThat(ctx.webhookRegistration(), is(WebhookRegistration.DISABLE)); + } + + @Test + public void given__webhookRegistrationFromItem__when__appliedToContext__then__webhookRegistrationFromItem() + throws Exception { + BitbucketSCMSourceContext ctx = new BitbucketSCMSourceContext(null, SCMHeadObserver.none()); + assumeThat(ctx.webhookRegistration(), is(WebhookRegistration.SYSTEM)); + WebhookRegistrationTrait instance = new WebhookRegistrationTrait(WebhookRegistration.ITEM.toString()); + instance.decorateContext(ctx); + assertThat(ctx.webhookRegistration(), is(WebhookRegistration.ITEM)); + } + + @Test + public void given__descriptor__when__displayingOptions__then__SYSTEM_not_present() { + ListBoxModel options = + j.jenkins.getDescriptorByType(WebhookRegistrationTrait.DescriptorImpl.class).doFillModeItems(); + for (ListBoxModel.Option o : options) { + assertThat(o.value, not(is(WebhookRegistration.SYSTEM.name()))); + } + } + +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/WebhooksAutoregisterTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/WebhooksAutoregisterTest.java index 79f9abec8..f7aa0ecf1 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/WebhooksAutoregisterTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/WebhooksAutoregisterTest.java @@ -23,27 +23,27 @@ */ package com.cloudbees.jenkins.plugins.bitbucket; +import com.cloudbees.jenkins.plugins.bitbucket.BranchScanningIntegrationTest.MultiBranchProjectImpl; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; +import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketEndpointConfiguration; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.WebhookAutoRegisterListener; +import hudson.model.listeners.ItemListener; +import hudson.util.RingBufferLogHandler; import java.io.File; import java.io.IOException; +import java.util.Collections; import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.logging.SimpleFormatter; - +import jenkins.branch.BranchSource; +import jenkins.branch.DefaultBranchPropertyStrategy; +import jenkins.model.JenkinsLocationConfiguration; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; import org.mockito.Mockito; -import org.xml.sax.SAXException; - -import com.cloudbees.jenkins.plugins.bitbucket.BranchScanningIntegrationTest.MultiBranchProjectImpl; -import com.cloudbees.jenkins.plugins.bitbucket.hooks.WebhookAutoRegisterListener; - -import hudson.util.RingBufferLogHandler; -import jenkins.branch.BranchSource; -import jenkins.branch.DefaultBranchPropertyStrategy; -import jenkins.model.JenkinsLocationConfiguration; public class WebhooksAutoregisterTest { @@ -53,7 +53,7 @@ public class WebhooksAutoregisterTest { @Test public void registerHookTest() throws Exception { BitbucketApi mock = Mockito.mock(BitbucketApi.class); - BitbucketMockApiFactory.add(null, mock); + BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, mock); RingBufferLogHandler log = createJULTestHandler(); MultiBranchProjectImpl p = j.jenkins.createProject(MultiBranchProjectImpl.class, "test"); @@ -70,7 +70,28 @@ public void registerHookTest() throws Exception { } - private void setRootUrl() throws IOException, SAXException, Exception { + @Test + public void registerHookTest2() throws Exception { + BitbucketEndpointConfiguration.get().setEndpoints(Collections.singletonList( + new BitbucketCloudEndpoint(true, "dummy"))); + BitbucketApi mock = Mockito.mock(BitbucketApi.class); + BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, mock); + RingBufferLogHandler log = createJULTestHandler(); + + MultiBranchProjectImpl p = j.jenkins.createProject(MultiBranchProjectImpl.class, "test"); + BitbucketSCMSource source = new BitbucketSCMSource(null, "amuniz", "test-repos"); + p.getSourcesList().add(new BranchSource(source)); + p.scheduleBuild2(0); + waitForLogFileMessage("Can not register hook. Jenkins root URL is not valid", log); + + setRootUrl(); + ItemListener.fireOnUpdated(p); + + waitForLogFileMessage("Registering hook for amuniz/test-repos", log); + + } + + private void setRootUrl() throws Exception { JenkinsLocationConfiguration.get().setUrl(j.getURL().toString().replace("localhost", "127.0.0.1")); } diff --git a/src/test/java/integration/ScanningFailuresTest.java b/src/test/java/integration/ScanningFailuresTest.java index e3f46ed63..283e2102c 100644 --- a/src/test/java/integration/ScanningFailuresTest.java +++ b/src/test/java/integration/ScanningFailuresTest.java @@ -8,6 +8,7 @@ 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.endpoints.BitbucketCloudEndpoint; import hudson.model.Result; import hudson.model.TopLevelItem; import java.io.IOException; @@ -125,7 +126,7 @@ private void getBranchesFails(Callable exception, Result expectedResu when(api.resolveCommit(sampleRepo.head())).thenReturn(commit); when(commit.getDateMillis()).thenReturn(System.currentTimeMillis()); - when(api.checkPathExists("master", "Jenkinsfile")).thenReturn(true); + when(api.checkPathExists(Mockito.anyString(), eq("Jenkinsfile"))).thenReturn(true); when(api.getRepositoryUri(eq(BitbucketRepositoryType.GIT), any(BitbucketRepositoryProtocol.class), @@ -140,7 +141,7 @@ private void getBranchesFails(Callable exception, Result expectedResu when(repository.getRepositoryName()).thenReturn("foo"); when(repository.getScm()).thenReturn("git"); - BitbucketMockApiFactory.add(null, api); + BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, api); WorkflowMultiBranchProject mp = j.jenkins.createProject(WorkflowMultiBranchProject.class, "smokes"); mp.getSourcesList().add(new BranchSource(new BitbucketSCMSource(null, "bob", "foo"))); @@ -191,7 +192,7 @@ public void checkPathExistsFails() throws Exception { when(api.resolveCommit(sampleRepo.head())).thenReturn(commit); when(commit.getDateMillis()).thenReturn(System.currentTimeMillis()); - when(api.checkPathExists("master", "Jenkinsfile")).thenReturn(true); + when(api.checkPathExists(Mockito.anyString(), eq("Jenkinsfile"))).thenReturn(true); when(api.getRepositoryUri(eq(BitbucketRepositoryType.GIT), any(BitbucketRepositoryProtocol.class), @@ -206,7 +207,7 @@ public void checkPathExistsFails() throws Exception { when(repository.getRepositoryName()).thenReturn("foo"); when(repository.getScm()).thenReturn("git"); - BitbucketMockApiFactory.add(null, api); + BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, api); WorkflowMultiBranchProject mp = j.jenkins.createProject(WorkflowMultiBranchProject.class, "smokes"); mp.getSourcesList().add(new BranchSource(new BitbucketSCMSource(null, "bob", "foo"))); @@ -218,7 +219,7 @@ public void checkPathExistsFails() throws Exception { assertThat(master, notNullValue()); // an error in checkPathExists(...) - when(api.checkPathExists("master", "Jenkinsfile")).thenThrow(new IOException(message)); + when(api.checkPathExists(Mockito.anyString(), eq("Jenkinsfile"))).thenThrow(new IOException(message)); mp.scheduleBuild2(0).getFuture().get(); assertThat(mp.getIndexing().getResult(), is(Result.FAILURE)); @@ -249,7 +250,7 @@ public void resolveCommitFails() throws Exception { when(api.resolveCommit(sampleRepo.head())).thenReturn(commit); when(commit.getDateMillis()).thenReturn(System.currentTimeMillis()); - when(api.checkPathExists("master", "Jenkinsfile")).thenReturn(true); + when(api.checkPathExists(Mockito.anyString(), eq("Jenkinsfile"))).thenReturn(true); when(api.getRepositoryUri(eq(BitbucketRepositoryType.GIT), any(BitbucketRepositoryProtocol.class), @@ -264,7 +265,7 @@ public void resolveCommitFails() throws Exception { when(repository.getRepositoryName()).thenReturn("foo"); when(repository.getScm()).thenReturn("git"); - BitbucketMockApiFactory.add(null, api); + BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, api); WorkflowMultiBranchProject mp = j.jenkins.createProject(WorkflowMultiBranchProject.class, "smokes"); mp.getSourcesList().add(new BranchSource(new BitbucketSCMSource(null, "bob", "foo"))); @@ -311,7 +312,7 @@ public void branchRemoved() throws Exception { when(api.resolveCommit(sampleRepo.head())).thenReturn(commit); when(commit.getDateMillis()).thenReturn(System.currentTimeMillis()); - when(api.checkPathExists("master", "Jenkinsfile")).thenReturn(true); + when(api.checkPathExists(Mockito.anyString(), eq("Jenkinsfile"))).thenReturn(true); when(api.getRepositoryUri(eq(BitbucketRepositoryType.GIT), any(BitbucketRepositoryProtocol.class), @@ -326,7 +327,7 @@ public void branchRemoved() throws Exception { when(repository.getRepositoryName()).thenReturn("foo"); when(repository.getScm()).thenReturn("git"); - BitbucketMockApiFactory.add(null, api); + BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, api); WorkflowMultiBranchProject mp = j.jenkins.createProject(WorkflowMultiBranchProject.class, "smokes"); mp.getSourcesList().add(new BranchSource(new BitbucketSCMSource(null, "bob", "foo"))); diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/basic_cloud.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/basic_cloud.xml new file mode 100644 index 000000000..994f0adfa --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/basic_cloud.xml @@ -0,0 +1,8 @@ + + cloudbeers + bcaef157-f105-407f-b150-df7722eab6c1 + SAME + .* + false + -1 + diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/basic_server.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/basic_server.xml new file mode 100644 index 000000000..c479f78b6 --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/basic_server.xml @@ -0,0 +1,11 @@ + + DUB + bitbucket + 8b2e4f77-39c5-41a9-b63b-8d367350bfdf + .* + false + https://bitbucket.test + * + + 7999 + diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/exclude_branches.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/exclude_branches.xml new file mode 100644 index 000000000..665f4d625 --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/exclude_branches.xml @@ -0,0 +1,10 @@ + + cloudbeers + bcaef157-f105-407f-b150-df7722eab6c1 + SAME + .* + false + * + master + -1 + diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/limit_branches.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/limit_branches.xml new file mode 100644 index 000000000..dbbd53177 --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/limit_branches.xml @@ -0,0 +1,10 @@ + + cloudbeers + bcaef157-f105-407f-b150-df7722eab6c1 + SAME + .* + false + feature/* + + -1 + diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/limit_repositories.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/limit_repositories.xml new file mode 100644 index 000000000..54acde132 --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/limit_repositories.xml @@ -0,0 +1,11 @@ + + DUB + bitbucket + 8b2e4f77-39c5-41a9-b63b-8d367350bfdf + limited.* + false + https://bitbucket.test + * + + 7999 + diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/modern.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/modern.xml new file mode 100644 index 000000000..a7bd928a6 --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/modern.xml @@ -0,0 +1,6 @@ + + https://bitbucket.org + cloudbeers + bcaef157-f105-407f-b150-df7722eab6c1 + + diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/register_hooks.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/register_hooks.xml new file mode 100644 index 000000000..2381aa959 --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/register_hooks.xml @@ -0,0 +1,11 @@ + + DUB + bitbucket + 8b2e4f77-39c5-41a9-b63b-8d367350bfdf + .* + true + https://bitbucket.test + * + + 7999 + diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/use_agent_checkout.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/use_agent_checkout.xml new file mode 100644 index 000000000..57f7c910c --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorTest/use_agent_checkout.xml @@ -0,0 +1,11 @@ + + DUB + bitbucket + ANONYMOUS + .* + false + https://bitbucket.test + * + + 7999 + diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/basic_cloud_git.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/basic_cloud_git.xml new file mode 100644 index 000000000..87edb8039 --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/basic_cloud_git.xml @@ -0,0 +1,14 @@ + + + com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure + + bitbucket-cloud + SAME + cloudbeers + stunning-adventure + * + + false + -1 + GIT + diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/basic_cloud_hg.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/basic_cloud_hg.xml new file mode 100644 index 000000000..12c7fc9f4 --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/basic_cloud_hg.xml @@ -0,0 +1,13 @@ + + com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::shiny-telegram + + bitbucket-cloud + SAME + cloudbeers + shiny-telegram + * + + false + -1 + MERCURIAL + diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/basic_server.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/basic_server.xml new file mode 100644 index 000000000..e2c621625 --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/basic_server.xml @@ -0,0 +1,14 @@ + + com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.test::DUB::stunning-adventure + + bb-beescloud + SAME + DUB + stunning-adventure + * + + false + https://bitbucket.test + 7999 + GIT + diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/custom_checkout_credentials.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/custom_checkout_credentials.xml new file mode 100644 index 000000000..1ad61472b --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/custom_checkout_credentials.xml @@ -0,0 +1,14 @@ + + + com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure + + bitbucket-cloud + other-credentials + cloudbeers + stunning-adventure + * + + false + -1 + GIT + diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/exclude_branches.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/exclude_branches.xml new file mode 100644 index 000000000..72403d596 --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/exclude_branches.xml @@ -0,0 +1,14 @@ + + + com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure + + bitbucket-cloud + SAME + cloudbeers + stunning-adventure + * + master + false + -1 + GIT + diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/limit_branches.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/limit_branches.xml new file mode 100644 index 000000000..3e90f4566 --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/limit_branches.xml @@ -0,0 +1,14 @@ + + + com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure + + bitbucket-cloud + SAME + cloudbeers + stunning-adventure + feature/* + + false + -1 + GIT + diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/modern.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/modern.xml new file mode 100644 index 000000000..d7cb0192e --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/modern.xml @@ -0,0 +1,8 @@ + + e4d8c11a-0d24-472f-b86b-4b017c160e9a + https://bitbucket.org + curl + cloudbeers + stunning-adventure + + diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/register_hooks.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/register_hooks.xml new file mode 100644 index 000000000..b2a22ea4e --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/register_hooks.xml @@ -0,0 +1,14 @@ + + + com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure + + bitbucket-cloud + SAME + cloudbeers + stunning-adventure + * + + true + -1 + GIT + diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/use_agent_checkout.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/use_agent_checkout.xml new file mode 100644 index 000000000..b1d93aa0a --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest/use_agent_checkout.xml @@ -0,0 +1,14 @@ + + + com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure + + bitbucket-cloud + ANONYMOUS + cloudbeers + stunning-adventure + * + + false + -1 + GIT +