From 43b5668811fef799fd140c725bae2fb564bc4d38 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Tue, 11 Apr 2017 17:18:09 +0100 Subject: [PATCH 01/40] [JENKINS-43433] Adapt to ChangeRequestSCMHead2 --- pom.xml | 2 +- .../plugins/bitbucket/BitbucketSCMSource.java | 7 +- .../plugins/bitbucket/PullRequestSCMHead.java | 149 +++++++++++++++++- .../bitbucket/SCMHeadWithOwnerAndRepo.java | 13 +- .../hooks/PullRequestHookProcessor.java | 12 +- 5 files changed, 172 insertions(+), 11 deletions(-) diff --git a/pom.xml b/pom.xml index e95457f55..38e436b05 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ 1.642.3 - 2.0.4 + 2.2.0-SNAPSHOT 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 68573021e..225c39256 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java @@ -77,6 +77,7 @@ import jenkins.scm.api.SCMHeadCategory; import jenkins.scm.api.SCMHeadEvent; import jenkins.scm.api.SCMHeadObserver; +import jenkins.scm.api.SCMHeadOrigin; import jenkins.scm.api.SCMRevision; import jenkins.scm.api.SCMSource; import jenkins.scm.api.SCMSourceCriteria; @@ -343,7 +344,11 @@ private void retrievePullRequests(SCMSourceCriteria criteria, SCMHeadObserver ob checkInterrupt(); PullRequestSCMHead head = new PullRequestSCMHead(pull.getSource().getRepository().getOwnerName(), pull.getSource().getRepository().getRepositoryName(), repositoryType, - pull.getSource().getBranch().getName(), pull); + 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; } 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 06afb69b0..9871607a9 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PullRequestSCMHead.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PullRequestSCMHead.java @@ -27,15 +27,25 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryType; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import hudson.Extension; import java.io.ObjectStreamException; +import jenkins.plugins.git.AbstractGitSCMSource; +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; +import org.kohsuke.accmod.restrictions.NoExternalUse; /** * {@link SCMHead} for a BitBucket Pull request * @since FIXME */ -public class PullRequestSCMHead extends SCMHead implements ChangeRequestSCMHead { +public class PullRequestSCMHead extends SCMHead implements ChangeRequestSCMHead2 { private static final String PR_BRANCH_PREFIX = "PR-"; @@ -51,27 +61,46 @@ public class PullRequestSCMHead extends SCMHead implements ChangeRequestSCMHead private final BranchSCMHead target; + private final SCMHeadOrigin origin; + public PullRequestSCMHead(String repoOwner, String repository, String branchName, - String number, BranchSCMHead target) { + String number, BranchSCMHead target, SCMHeadOrigin origin) { super(PR_BRANCH_PREFIX + number); this.repoOwner = repoOwner; this.repository = repository; this.branchName = branchName; this.number = number; this.target = target; + this.origin = origin; + } + + @Deprecated + @Restricted(DoNotUse.class) + public PullRequestSCMHead(String repoOwner, String repository, String branchName, + String number, BranchSCMHead target) { + this(repoOwner, repository, branchName, number, target, null); } + @Deprecated + @Restricted(DoNotUse.class) public PullRequestSCMHead(String repoOwner, String repository, String branchName, BitbucketPullRequest pr) { - this(repoOwner, repository, null, branchName, pr); + this(repoOwner, repository, null, branchName, pr, null); } + @Deprecated + @Restricted(DoNotUse.class) 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; } @SuppressFBWarnings("SE_PRIVATE_READ_RESOLVE_NOT_INHERITED") // because JENKINS-41313 @@ -80,6 +109,10 @@ 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); + } return this; } @@ -111,4 +144,114 @@ public SCMHead getTarget() { return target; } + @NonNull + @Override + public ChangeRequestCheckoutStrategy getCheckoutStrategy() { + return ChangeRequestCheckoutStrategy.HEAD; // TODO add support + } + + @NonNull + @Override + public String getOriginName() { + return branchName; + } + + @NonNull + @Override + public SCMHeadOrigin getOrigin() { + return origin == null ? SCMHeadOrigin.DEFAULT : origin; + } + + /** + * Used to handle data migration. + * + * @see FixOriginMigration1 + * @see FixOriginMigration2 + * @deprecated used for data migration. + */ + @Deprecated + @Restricted(NoExternalUse.class) + public static class FixOrigin extends PullRequestSCMHead { + + FixOrigin(PullRequestSCMHead copy) { + super(copy.repoOwner, copy.repository, copy.branchName, copy.number, copy.target, null); + } + } + + /** + * Used to handle data migration. + * + * @deprecated used for data migration. + */ + @Restricted(NoExternalUse.class) + @Extension + public static class FixOriginMigration1 extends + SCMHeadMigration { + public FixOriginMigration1() { + super(BitbucketSCMSource.class, FixOrigin.class, AbstractGitSCMSource.SCMRevisionImpl.class); + } + + @Override + public PullRequestSCMHead migrate(@NonNull BitbucketSCMSource source, @NonNull FixOrigin head) { + return new PullRequestSCMHead( + head.getName(), + head.getRepository(), + head.getBranchName(), + head.getId(), + (BranchSCMHead) head.getTarget(), + source.getRepoOwner().equalsIgnoreCase(head.getRepoOwner()) + ? SCMHeadOrigin.DEFAULT + : new SCMHeadOrigin.Fork(head.getRepoOwner()) + ); + } + + @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() + ) : null; + } + } + + /** + * Used to handle data migration. + * + * @deprecated used for data migration. + */ + @Restricted(NoExternalUse.class) + @Extension + public static class FixOriginMigration2 extends + SCMHeadMigration { + public FixOriginMigration2() { + super(BitbucketSCMSource.class, FixOrigin.class, BitbucketSCMSource.MercurialRevision.class); + } + + @Override + public PullRequestSCMHead migrate(@NonNull BitbucketSCMSource source, @NonNull FixOrigin head) { + return new PullRequestSCMHead( + head.getName(), + head.getRepository(), + head.getBranchName(), + head.getId(), + (BranchSCMHead) head.getTarget(), + source.getRepoOwner().equalsIgnoreCase(head.getRepoOwner()) + ? SCMHeadOrigin.DEFAULT + : new SCMHeadOrigin.Fork(head.getRepoOwner()) + ); + } + + @Override + public SCMRevision migrate(@NonNull BitbucketSCMSource source, + @NonNull BitbucketSCMSource.MercurialRevision revision) { + PullRequestSCMHead head = migrate(source, (FixOrigin) revision.getHead()); + return head != null ? new BitbucketSCMSource.MercurialRevision( + head, + revision.getHash() + ) : null; + } + } + } 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 922a22296..e9c9f58a6 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SCMHeadWithOwnerAndRepo.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SCMHeadWithOwnerAndRepo.java @@ -40,6 +40,7 @@ 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.ChangeRequestSCMHead; import org.kohsuke.accmod.Restricted; @@ -92,7 +93,7 @@ public static class PR extends PullRequestSCMHead { public PR(String repoOwner, String repository, String branchName, String number, BranchSCMHead target) { - super(repoOwner, repository, branchName, number, target); + super(repoOwner, repository, branchName, number, target, null); } } @@ -144,7 +145,10 @@ public SCMHead migrate(@NonNull BitbucketSCMSource source, @NonNull PR head) { target = "\u0000"; } return new PullRequestSCMHead(head.getRepoOwner(), head.getRepository(), head.getBranchName(), head.getId(), - new BranchSCMHead(target, BitbucketRepositoryType.MERCURIAL)); + new BranchSCMHead(target, BitbucketRepositoryType.MERCURIAL), + source.getRepoOwner().equalsIgnoreCase(head.getRepoOwner()) + ? SCMHeadOrigin.DEFAULT + : new SCMHeadOrigin.Fork(head.getRepoOwner())); } @Override @@ -174,7 +178,10 @@ public SCMHead migrate(@NonNull BitbucketSCMSource source, @NonNull PR head) { target = "\u0000"; } return new PullRequestSCMHead(head.getRepoOwner(), head.getRepository(), head.getBranchName(), head.getId(), - new BranchSCMHead(target, BitbucketRepositoryType.GIT)); + new BranchSCMHead(target, BitbucketRepositoryType.GIT), + source.getRepoOwner().equalsIgnoreCase(head.getRepoOwner()) + ? SCMHeadOrigin.DEFAULT + : new SCMHeadOrigin.Fork(head.getRepoOwner())); } @Override 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 6d669b944..ada9a446d 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 @@ -35,7 +35,6 @@ import com.cloudbees.jenkins.plugins.bitbucket.server.events.BitbucketServerPullRequestEvent; 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; @@ -47,10 +46,10 @@ import jenkins.scm.api.SCMEvent; import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMHeadEvent; +import jenkins.scm.api.SCMHeadOrigin; import jenkins.scm.api.SCMNavigator; import jenkins.scm.api.SCMRevision; import jenkins.scm.api.SCMSource; -import org.codehaus.jackson.map.ObjectMapper; import static com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType.PULL_REQUEST_DECLINED; import static com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType.PULL_REQUEST_MERGED; @@ -162,7 +161,14 @@ public Map heads(@NonNull SCMSource source) { getPayload().getPullRequest().getSource().getRepository().getRepositoryName(), type, getPayload().getPullRequest().getSource().getBranch().getName(), - getPayload().getPullRequest() + 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 From 301f8d0b5208f7cce624b26833d6fd37215a09f5 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Tue, 11 Apr 2017 17:44:47 +0100 Subject: [PATCH 02/40] [JENKINS-43433] Pick up scm api snapshot --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 38e436b05..5aabb9ab4 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ 1.642.3 - 2.2.0-SNAPSHOT + 2.2.0-20170411.160059-4 From b8f9be36cac446e8855cdf51f31fae6b54ece239 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Mon, 29 May 2017 16:13:04 +0100 Subject: [PATCH 03/40] [JENKINS-43507] Add endpoints to global configuration --- .../endpoints/AbstractBitbucketEndpoint.java | 129 +++++++++ .../AbstractBitbucketEndpointDescriptor.java | 66 +++++ .../endpoints/BitbucketCloudEndpoint.java | 88 ++++++ .../BitbucketEndpointConfiguration.java | 268 ++++++++++++++++++ .../endpoints/BitbucketServerEndpoint.java | 120 ++++++++ .../AbstractBitbucketEndpoint/config.jelly | 10 + .../help-credentialsId.html | 4 + .../help-manageHooks.html | 6 + .../config-detail.jelly | 7 + .../config.jelly | 15 + .../config-detail.jelly | 10 + .../help-displayName.html | 3 + .../help-serverUrl.html | 4 + .../bitbucket/endpoints/Messages.properties | 2 + 14 files changed, 732 insertions(+) create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpointDescriptor.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpoint.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint.java create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint/config.jelly create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint/help-credentialsId.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint/help-manageHooks.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpoint/config-detail.jelly create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration/config.jelly create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint/config-detail.jelly create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint/help-displayName.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint/help-serverUrl.html create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/Messages.properties diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint.java new file mode 100644 index 000000000..73dfcd1b7 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint.java @@ -0,0 +1,129 @@ +/* + * 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.endpoints; + +import com.cloudbees.plugins.credentials.CredentialsMatchers; +import com.cloudbees.plugins.credentials.CredentialsProvider; +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 hudson.model.AbstractDescribableImpl; +import hudson.security.ACL; +import jenkins.model.Jenkins; +import org.apache.commons.lang.StringUtils; + +/** + * Represents a {@link BitbucketCloudEndpoint} or a {@link BitbucketServerEndpoint}. + * + * @since 2.2.0 + */ +public abstract class AbstractBitbucketEndpoint extends AbstractDescribableImpl { + + /** + * {@code true} if and only if Jenkins is supposed to auto-manage hooks for this end-point. + */ + private final boolean manageHooks; + + /** + * The {@link StandardUsernamePasswordCredentials#getId()} of the credentials to use for auto-management of hooks. + */ + @CheckForNull + private final String credentialsId; + + /** + * Constructor. + * + * @param manageHooks {@code true} if and only if Jenkins is supposed to auto-manage hooks for this end-point. + * @param credentialsId The {@link StandardUsernamePasswordCredentials#getId()} of the credentials to use for + * auto-management of hooks. + */ + AbstractBitbucketEndpoint(boolean manageHooks, @CheckForNull String credentialsId) { + this.manageHooks = manageHooks && StringUtils.isNotBlank(credentialsId); + this.credentialsId = manageHooks ? credentialsId : null; + } + + /** + * Optional name to use to describe the end-point. + * + * @return the name to use for the end-point + */ + @CheckForNull + public abstract String getDisplayName(); + + /** + * The URL of this endpoint. + * + * @return the URL of the endpoint. + */ + @NonNull + public abstract String getServerUrl(); + + /** + * Returns {@code true} if and only if Jenkins is supposed to auto-manage hooks for this end-point. + * + * @return {@code true} if and only if Jenkins is supposed to auto-manage hooks for this end-point. + */ + public final boolean isManageHooks() { + return manageHooks; + } + + /** + * Returns the {@link StandardUsernamePasswordCredentials#getId()} of the credentials to use for auto-management + * of hooks. + * + * @return the {@link StandardUsernamePasswordCredentials#getId()} of the credentials to use for auto-management + * of hooks. + */ + @CheckForNull + public final String getCredentialsId() { + return credentialsId; + } + + /** + * Looks up the {@link StandardUsernamePasswordCredentials} to use for auto-management of hooks. + * + * @return the credentials or {@code null}. + */ + @CheckForNull + public StandardUsernamePasswordCredentials credentials() { + return StringUtils.isBlank(credentialsId) ? null : CredentialsMatchers.firstOrNull( + CredentialsProvider.lookupCredentials( + StandardUsernamePasswordCredentials.class, + Jenkins.getActiveInstance(), + ACL.SYSTEM, + URIRequirementBuilder.fromUri(getServerUrl()).build() + ), + CredentialsMatchers.withId(credentialsId) + ); + } + + /** + * {@inheritDoc} + */ + @Override + public AbstractBitbucketEndpointDescriptor getDescriptor() { + return (AbstractBitbucketEndpointDescriptor) super.getDescriptor(); + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpointDescriptor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpointDescriptor.java new file mode 100644 index 000000000..40cfb0453 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpointDescriptor.java @@ -0,0 +1,66 @@ +/* + * 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.endpoints; + +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.common.StandardUsernamePasswordCredentials; +import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; +import hudson.model.Descriptor; +import hudson.security.ACL; +import hudson.util.ListBoxModel; +import jenkins.model.Jenkins; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.QueryParameter; + +/** + * {@link Descriptor} base class for {@link AbstractBitbucketEndpoint} subclasses. + * + * @since 2.2.0 + */ +public abstract class AbstractBitbucketEndpointDescriptor extends Descriptor { + /** + * Stapler form completion. + * + * @param serverUrl the server URL. + * @return the available credentials. + */ + @Restricted(NoExternalUse.class) // stapler + @SuppressWarnings("unused") + public ListBoxModel doFillCredentialsIdItems(@QueryParameter String serverUrl) { + Jenkins jenkins = Jenkins.getActiveInstance(); + jenkins.checkPermission(Jenkins.ADMINISTER); + StandardListBoxModel result = new StandardListBoxModel(); + result.includeMatchingAs( + ACL.SYSTEM, + jenkins, + StandardUsernameCredentials.class, + URIRequirementBuilder.fromUri(serverUrl).build(), + CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class) + ); + return result; + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpoint.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpoint.java new file mode 100644 index 000000000..8caf44805 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpoint.java @@ -0,0 +1,88 @@ +/* + * 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.endpoints; + +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 javax.annotation.Nonnull; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Represents Bitbucket Cloud. + * + * @since 2.2.0 + */ +public class BitbucketCloudEndpoint extends AbstractBitbucketEndpoint { + + /** + * The URL of Bitbucket Cloud. + */ + public static final String SERVER_URL = "https://bitbucket.org"; + + /** + * Constructor. + * + * @param manageHooks {@code true} if and only if Jenkins is supposed to auto-manage hooks for this end-point. + * @param credentialsId The {@link StandardUsernamePasswordCredentials#getId()} of the credentials to use for + * auto-management of hooks. + */ + @DataBoundConstructor + public BitbucketCloudEndpoint(boolean manageHooks, @CheckForNull String credentialsId) { + super(manageHooks, credentialsId); + } + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return Messages.BitbucketCloudEndpoint_displayName(); + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public String getServerUrl() { + return SERVER_URL; + } + + /** + * Our descriptor. + */ + @Extension + public static class DescriptorImpl extends AbstractBitbucketEndpointDescriptor { + /** + * {@inheritDoc} + */ + @Nonnull + @Override + public String getDisplayName() { + return Messages.BitbucketCloudEndpoint_displayName(); + } + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java new file mode 100644 index 000000000..f2da9abc5 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java @@ -0,0 +1,268 @@ +/* + * 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.endpoints; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.ExtensionList; +import hudson.security.ACL; +import hudson.util.ListBoxModel; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import jenkins.model.GlobalConfiguration; +import jenkins.model.Jenkins; +import net.sf.json.JSONObject; +import org.apache.commons.lang.StringUtils; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.StaplerRequest; + +/** + * Represents the global configuration of Bitbucket Cloud and Bitbucket Server endpoints. + * + * @since 2.2.0 + */ +@Extension +public class BitbucketEndpointConfiguration extends GlobalConfiguration { + + /** + * The list of {@link AbstractBitbucketEndpoint}, this is subject to the constraint that there can only ever be + * one entry for each {@link AbstractBitbucketEndpoint#getServerUrl()}. + */ + private List endpoints; + + /** + * Constructor. + */ + public BitbucketEndpointConfiguration() { + load(); + } + + /** + * Gets the {@link BitbucketEndpointConfiguration} singleton. + * + * @return the {@link BitbucketEndpointConfiguration} singleton. + */ + public static BitbucketEndpointConfiguration get() { + return ExtensionList.lookup(GlobalConfiguration.class).get(BitbucketEndpointConfiguration.class); + } + + /** + * Called from a {@code readResolve()} method only to convert the old {@code bitbucketServerUrl} field into the new + * {@code serverUrl} field. When called from {@link ACL#SYSTEM} this will update the configuration with the + * missing definitions of resolved URLs. + * + * @param bitbucketServerUrl the value of the old url field. + * @return the value of the new url field. + */ + @Restricted(NoExternalUse.class) // only for plugin internal use. + @NonNull + public String readResolveServerUrl(@CheckForNull String bitbucketServerUrl) { + String serverUrl = normalizeServerUrl(bitbucketServerUrl); + AbstractBitbucketEndpoint endpoint = findEndpoint(serverUrl); + if (endpoint == null && ACL.SYSTEM.equals(Jenkins.getAuthentication())) { + if (BitbucketCloudEndpoint.SERVER_URL.equals(serverUrl)) { + // exception case + addEndpoint(new BitbucketCloudEndpoint(false, null)); + } else { + addEndpoint(new BitbucketServerEndpoint(null, serverUrl, false, null)); + } + } + return endpoint == null ? serverUrl : endpoint.getServerUrl(); + } + + /** + * Returns {@code true} if and only if there is more than one configured endpoint. + * + * @return {@code true} if and only if there is more than one configured endpoint. + */ + public boolean isEndpointSelectable() { + return get().getEndpoints().size() > 1; + } + + /** + * Populates a {@link ListBoxModel} with the endpoints. + * + * @return A {@link ListBoxModel} with all the endpoints + */ + public ListBoxModel getEndpointItems() { + ListBoxModel result = new ListBoxModel(); + for (AbstractBitbucketEndpoint endpoint : getEndpoints()) { + String serverUrl = endpoint.getServerUrl(); + String displayName = endpoint.getDisplayName(); + result.add(StringUtils.isBlank(displayName) ? serverUrl : displayName + " (" + serverUrl + ")", serverUrl); + } + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean configure(StaplerRequest req, JSONObject json) throws FormException { + req.bindJSON(this, json); + return true; + } + + /** + * Gets the list of endpoints. + * + * @return the list of endpoints + */ + @NonNull + public synchronized List getEndpoints() { + return endpoints == null || endpoints.isEmpty() + ? Collections.singletonList(new BitbucketCloudEndpoint(false, null)) + : Collections.unmodifiableList(endpoints); + } + + /** + * Sets the list of endpoints. + * + * @param endpoints the list of endpoints. + */ + public synchronized void setEndpoints(@CheckForNull List endpoints) { + endpoints = new ArrayList( + endpoints == null ? Collections.emptyList() : endpoints); + // remove duplicates and empty urls + Set serverUrls = new HashSet(); + for (Iterator iterator = endpoints.iterator(); iterator.hasNext(); ) { + AbstractBitbucketEndpoint endpoint = iterator.next(); + String serverUrl = endpoint.getServerUrl(); + if (StringUtils.isBlank(serverUrl) || serverUrls.contains(serverUrl)) { + iterator.remove(); + } + serverUrls.add(serverUrl); + } + if (endpoints.isEmpty()) { + endpoints.add(new BitbucketCloudEndpoint(false, null)); + } + this.endpoints = endpoints; + save(); + } + + /** + * Adds an endpoint. + * + * @param endpoint the endpoint to add. + * @return {@code true} if the list of endpoints was modified + */ + public synchronized boolean addEndpoint(@NonNull AbstractBitbucketEndpoint endpoint) { + List endpoints = new ArrayList<>(getEndpoints()); + for (AbstractBitbucketEndpoint ep : endpoints) { + if (ep.getServerUrl().equals(endpoint.getServerUrl())) { + return false; + } + } + endpoints.add(endpoint); + setEndpoints(endpoints); + return true; + } + + /** + * Updates an existing endpoint (or adds if missing). + * + * @param endpoint the endpoint to update. + */ + public synchronized void updateEndpoint(@NonNull AbstractBitbucketEndpoint endpoint) { + List endpoints = new ArrayList<>(getEndpoints()); + boolean found = false; + for (int i = 0; i < endpoints.size(); i++) { + AbstractBitbucketEndpoint ep = endpoints.get(i); + if (ep.getServerUrl().equals(endpoint.getServerUrl())) { + endpoints.set(i, endpoint); + found = true; + break; + } + } + if (!found) { + endpoints.add(endpoint); + } + setEndpoints(endpoints); + } + + /** + * Removes an endpoint. + * + * @param endpoint the endpoint to remove. + * @return {@code true} if the list of endpoints was modified + */ + public boolean removeEndpoint(@NonNull AbstractBitbucketEndpoint endpoint) { + return removeEndpoint(endpoint.getServerUrl()); + } + + /** + * Removes an endpoint. + * + * @param serverUrl the server URL to remove. + * @return {@code true} if the list of endpoints was modified + */ + public synchronized boolean removeEndpoint(@CheckForNull String serverUrl) { + serverUrl = normalizeServerUrl(serverUrl); + boolean modified = false; + List endpoints = new ArrayList<>(getEndpoints()); + for (Iterator iterator = endpoints.iterator(); iterator.hasNext(); ) { + if (serverUrl.equals(iterator.next().getServerUrl())) { + iterator.remove(); + modified = true; + } + } + setEndpoints(endpoints); + return modified; + } + + /** + * Checks to see if the supplied server URL is defined in the global configuration. + * + * @param serverUrl the server url to check. + * @return the global configuration for the specified server url or {@code null} if not defined. + */ + @CheckForNull + public synchronized AbstractBitbucketEndpoint findEndpoint(@CheckForNull String serverUrl) { + serverUrl = normalizeServerUrl(serverUrl); + for (AbstractBitbucketEndpoint endpoint : getEndpoints()) { + if (serverUrl.equals(endpoint.getServerUrl())) { + return endpoint; + } + } + return null; + } + + /** + * Fix a serverUrl. + * + * @param serverUrl the server URL. + * @return the normalized server URL. + */ + @NonNull + public static String normalizeServerUrl(@CheckForNull String serverUrl) { + return StringUtils.defaultIfBlank(serverUrl, BitbucketCloudEndpoint.SERVER_URL).replaceAll("/$", ""); + } + +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint.java new file mode 100644 index 000000000..c1e64d192 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint.java @@ -0,0 +1,120 @@ +/* + * 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.endpoints; + +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.Util; +import hudson.util.FormValidation; +import java.net.MalformedURLException; +import java.net.URL; +import javax.annotation.Nonnull; +import org.apache.commons.lang.StringUtils; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; + +/** + * Represents a Bitbucket Server instance. + * + * @since 2.2.0 + */ +public class BitbucketServerEndpoint extends AbstractBitbucketEndpoint { + + /** + * Optional name to use to describe the end-point. + */ + @CheckForNull + private final String displayName; + + /** + * The URL of this Bitbucket Server. + */ + @NonNull + private final String serverUrl; + + /** + * @param displayName Optional name to use to describe the end-point. + * @param serverUrl The URL of this Bitbucket Server + * @param manageHooks {@code true} if and only if Jenkins is supposed to auto-manage hooks for this end-point. + * @param credentialsId The {@link StandardUsernamePasswordCredentials#getId()} of the credentials to use for + * auto-management of hooks. + */ + @DataBoundConstructor + public BitbucketServerEndpoint(@CheckForNull String displayName, @NonNull String serverUrl, boolean manageHooks, + @CheckForNull String credentialsId) { + super(manageHooks, credentialsId); + this.displayName = Util.fixEmpty(displayName); + this.serverUrl = StringUtils.defaultString(serverUrl).replaceAll("/$", ""); + } + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return displayName; + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public String getServerUrl() { + return serverUrl; + } + + /** + * Our descriptor. + */ + @Extension + public static class DescriptorImpl extends AbstractBitbucketEndpointDescriptor { + /** + * {@inheritDoc} + */ + @Nonnull + @Override + public String getDisplayName() { + return Messages.BitbucketServerEndpoint_displayName(); + } + + /** + * Checks that the supplied URL is valid. + * + * @param value the URL to check. + * @return the validation results. + */ + public static FormValidation doCheckServerUrl(@QueryParameter String value) { + try { + new URL(value); + } catch (MalformedURLException e) { + return FormValidation.error("Invalid URL: " + e.getMessage()); + } + return FormValidation.ok(); + } + + } +} diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint/config.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint/config.jelly new file mode 100644 index 000000000..4dd969dd6 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint/config.jelly @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint/help-credentialsId.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint/help-credentialsId.html new file mode 100644 index 000000000..7d451cd04 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint/help-credentialsId.html @@ -0,0 +1,4 @@ +
+ Select the credentials to use for managing hooks. Both GLOBAL and SYSTEM scoped credentials are eligible as the + management of hooks is run in the context of Jenkins itself and not in the context of the individual items. +
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint/help-manageHooks.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint/help-manageHooks.html new file mode 100644 index 000000000..fc07fc0ed --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint/help-manageHooks.html @@ -0,0 +1,6 @@ +
+ Selecting this option will enable the automatic management of web hooks for all items that use this + endpoint, with the exception of those items that have explicitly opted out of hook management. + When this option is not selected, individual items can still opt in to hook management provided the credentials + those items have been configured with have permission to manage the required hooks. +
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpoint/config-detail.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpoint/config-detail.jelly new file mode 100644 index 000000000..72bab88ef --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpoint/config-detail.jelly @@ -0,0 +1,7 @@ + + + + + + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration/config.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration/config.jelly new file mode 100644 index 000000000..fc05bcaa7 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration/config.jelly @@ -0,0 +1,15 @@ + + + + + + +
+ +
+
+
+
+
+
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint/config-detail.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint/config-detail.jelly new file mode 100644 index 000000000..3460604c6 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint/config-detail.jelly @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint/help-displayName.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint/help-displayName.html new file mode 100644 index 000000000..153bdaad0 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint/help-displayName.html @@ -0,0 +1,3 @@ +
+ The human friendly name for this Bitbucket server endpoint. +
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint/help-serverUrl.html b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint/help-serverUrl.html new file mode 100644 index 000000000..9b181f4bf --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint/help-serverUrl.html @@ -0,0 +1,4 @@ +
+ The URL of this Bitbucket Server, examples include: http://internal.example.com:7990/bitbucket + and https://bitbucket.example.com +
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/Messages.properties b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/Messages.properties new file mode 100644 index 000000000..88d74f200 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/endpoints/Messages.properties @@ -0,0 +1,2 @@ +BitbucketCloudEndpoint.displayName=Bitbucket Cloud +BitbucketServerEndpoint.displayName=Bitbucket Server From 3530fcae07c77c0be6028ec705938d171d681d1a Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Mon, 29 May 2017 17:38:06 +0100 Subject: [PATCH 04/40] [JENKINS-43507] Add endpoints tests --- ...stractBitbucketEndpointDescriptorTest.java | 105 ++++++++++++++++ .../AbstractBitbucketEndpointTest.java | 119 ++++++++++++++++++ .../endpoints/BitbucketCloudEndpointTest.java | 40 ++++++ .../BitbucketServerEndpointTest.java | 41 ++++++ 4 files changed, 305 insertions(+) create mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpointDescriptorTest.java create mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpointTest.java create mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpointTest.java create mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpointTest.java diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpointDescriptorTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpointDescriptorTest.java new file mode 100644 index 000000000..b49d2a365 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpointDescriptorTest.java @@ -0,0 +1,105 @@ +/* + * 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.endpoints; + +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.domains.DomainSpecification; +import com.cloudbees.plugins.credentials.domains.HostnameSpecification; +import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.util.ListBoxModel; +import java.util.Collections; +import java.util.List; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestExtension; + +import static org.junit.Assert.assertThat; + +public class AbstractBitbucketEndpointDescriptorTest { + + @ClassRule + public static JenkinsRule j = new JenkinsRule(); + + @Before + public void reset() { + SystemCredentialsProvider.getInstance() + .setDomainCredentialsMap(Collections.>emptyMap()); + } + + @Test + public void given__cloudCredentials__when__listingForServer__then__noCredentials() { + SystemCredentialsProvider.getInstance().setDomainCredentialsMap(Collections.singletonMap( + new Domain("cloud", "bb cloud", + Collections.singletonList(new HostnameSpecification("bitbucket.org", ""))), + Collections.singletonList(new UsernamePasswordCredentialsImpl( + CredentialsScope.SYSTEM, "dummy", "dummy", "user", "pass")))); + ListBoxModel result = + new Dummy(true, "dummy").getDescriptor().doFillCredentialsIdItems("http://bitbucket.example.com"); + assertThat(result, Matchers.hasSize(0)); + } + + @Test + public void given__cloudCredentials__when__listingForCloud__then__credentials() { + SystemCredentialsProvider.getInstance().setDomainCredentialsMap(Collections.singletonMap( + new Domain("cloud", "bb cloud", + Collections.singletonList(new HostnameSpecification("bitbucket.org", ""))), + Collections.singletonList(new UsernamePasswordCredentialsImpl( + CredentialsScope.SYSTEM, "dummy", "dummy", "user", "pass")))); + ListBoxModel result = + new Dummy(true, "dummy").getDescriptor().doFillCredentialsIdItems("http://bitbucket.org"); + assertThat(result, Matchers.hasSize(1)); + } + + public static class Dummy extends AbstractBitbucketEndpoint { + + Dummy(boolean manageHooks, String credentialsId) { + super(manageHooks, credentialsId); + } + + @Override + public String getDisplayName() { + return "Dummy"; + } + + @NonNull + @Override + public String getServerUrl() { + return "http://dummy.example.com"; + } + + @TestExtension + public static class DescriptorImpl extends AbstractBitbucketEndpointDescriptor { + + } + } + + +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpointTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpointTest.java new file mode 100644 index 000000000..2a4d9edb9 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpointTest.java @@ -0,0 +1,119 @@ +/* + * 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.endpoints; + +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 edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Collections; +import java.util.List; +import org.junit.Before; +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.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +public class AbstractBitbucketEndpointTest { + + @ClassRule + public static JenkinsRule j = new JenkinsRule(); + + @Before + public void reset() { + SystemCredentialsProvider.getInstance() + .setDomainCredentialsMap(Collections.>emptyMap()); + } + + @Test + public void given__manage_true__when__noCredentials__then__manage_false() { + assertThat(new Dummy(true, null).isManageHooks(), is(false)); + } + + @Test + public void given__manage_false__when__credentials__then__manage_false() { + assertThat(new Dummy(false, "dummy").isManageHooks(), is(false)); + } + + @Test + public void given__manage_false__when__credentials__then__credentials_null() { + assertThat(new Dummy(false, "dummy").getCredentialsId(), is(nullValue())); + } + + @Test + public void given__manage_true__when__credentials__then__manage_true() { + assertThat(new Dummy(true, "dummy").isManageHooks(), is(true)); + } + + @Test + public void given__manage_true__when__credentials__then__credentialsSet() { + assertThat(new Dummy(true, "dummy").getCredentialsId(), is("dummy")); + } + + @Test + public void given__mange__when__systemCredentials__then__credentialsFound() { + SystemCredentialsProvider.getInstance().setDomainCredentialsMap(Collections.singletonMap(Domain.global(), + Collections.singletonList(new UsernamePasswordCredentialsImpl( + CredentialsScope.SYSTEM, "dummy", "dummy", "user", "pass")))); + assertThat(new Dummy(true, "dummy").credentials(), notNullValue()); + } + + @Test + public void given__mange__when__globalCredentials__then__credentialsFound() { + SystemCredentialsProvider.getInstance().setDomainCredentialsMap(Collections.singletonMap(Domain.global(), + Collections.singletonList(new UsernamePasswordCredentialsImpl( + CredentialsScope.GLOBAL, "dummy", "dummy", "user", "pass")))); + assertThat(new Dummy(true, "dummy").credentials(), notNullValue()); + } + + @Test + public void given__mange__when__noCredentials__then__credentials_none() { + assertThat(new Dummy(true, "dummy").credentials(), nullValue()); + } + + private static class Dummy extends AbstractBitbucketEndpoint { + + Dummy(boolean manageHooks, String credentialsId) { + super(manageHooks, credentialsId); + } + + @Override + public String getDisplayName() { + return "Dummy"; + } + + @NonNull + @Override + public String getServerUrl() { + return "http://dummy.example.com"; + } + } + +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpointTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpointTest.java new file mode 100644 index 000000000..203d34e39 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpointTest.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.endpoints; + +import org.junit.Test; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +public class BitbucketCloudEndpointTest { + + @Test + public void smokes() { + assertThat(new BitbucketCloudEndpoint(false, null).getDisplayName(), notNullValue()); + assertThat(new BitbucketCloudEndpoint(false, null).getServerUrl(), is(BitbucketCloudEndpoint.SERVER_URL)); + } + +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpointTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpointTest.java new file mode 100644 index 000000000..f403a1bac --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpointTest.java @@ -0,0 +1,41 @@ +/* + * 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.endpoints; + +import org.junit.Test; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +public class BitbucketServerEndpointTest { + + + @Test + public void smokes() { + assertThat(new BitbucketServerEndpoint("Dummy", "http://dummy.example.com", false, null).getDisplayName(), + is("Dummy")); + assertThat(new BitbucketServerEndpoint("Dummy", "http://dummy.example.com", false, null).getServerUrl(), is( + "http://dummy.example.com")); + } +} From dfd47ff7fbc107ba336d1025355277f634bcc429 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Tue, 30 May 2017 15:35:23 +0100 Subject: [PATCH 05/40] [JENKINS-43507] Add endpoints to global configuration --- .../BitbucketEndpointConfiguration.java | 48 +- .../endpoints/BitbucketServerEndpoint.java | 3 +- .../BitbucketEndpointConfigurationTest.java | 618 ++++++++++++++++++ .../BitbucketServerEndpointTest.java | 14 + 4 files changed, 676 insertions(+), 7 deletions(-) create mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfigurationTest.java diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java index f2da9abc5..8845b3559 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java @@ -27,13 +27,18 @@ import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.ExtensionList; +import hudson.Util; import hudson.security.ACL; import hudson.util.ListBoxModel; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.ListIterator; +import java.util.Locale; import java.util.Set; import jenkins.model.GlobalConfiguration; import jenkins.model.Jenkins; @@ -103,7 +108,7 @@ public String readResolveServerUrl(@CheckForNull String bitbucketServerUrl) { * @return {@code true} if and only if there is more than one configured endpoint. */ public boolean isEndpointSelectable() { - return get().getEndpoints().size() > 1; + return getEndpoints().size() > 1; } /** @@ -148,15 +153,19 @@ public synchronized List getEndpoints() { * @param endpoints the list of endpoints. */ public synchronized void setEndpoints(@CheckForNull List endpoints) { - endpoints = new ArrayList( - endpoints == null ? Collections.emptyList() : endpoints); + Jenkins.getActiveInstance().checkPermission(Jenkins.ADMINISTER); + endpoints = new ArrayList<>(Util.fixNull(endpoints)); // remove duplicates and empty urls Set serverUrls = new HashSet(); - for (Iterator iterator = endpoints.iterator(); iterator.hasNext(); ) { + for (ListIterator iterator = endpoints.listIterator(); iterator.hasNext(); ) { AbstractBitbucketEndpoint endpoint = iterator.next(); String serverUrl = endpoint.getServerUrl(); if (StringUtils.isBlank(serverUrl) || serverUrls.contains(serverUrl)) { iterator.remove(); + } else if (!(endpoint instanceof BitbucketCloudEndpoint) + && BitbucketCloudEndpoint.SERVER_URL.equals(serverUrl)) { + // fix type for the special case + iterator.set(new BitbucketCloudEndpoint(endpoint.isManageHooks(), endpoint.getCredentialsId())); } serverUrls.add(serverUrl); } @@ -262,7 +271,36 @@ public synchronized AbstractBitbucketEndpoint findEndpoint(@CheckForNull String */ @NonNull public static String normalizeServerUrl(@CheckForNull String serverUrl) { - return StringUtils.defaultIfBlank(serverUrl, BitbucketCloudEndpoint.SERVER_URL).replaceAll("/$", ""); + serverUrl = StringUtils.defaultIfBlank(serverUrl, BitbucketCloudEndpoint.SERVER_URL); + try { + URI uri = new URI(serverUrl).normalize(); + String scheme = uri.getScheme(); + if ("http".equals(scheme) || "https".equals(scheme)) { + // we only expect http / https, but also these are the only ones where we know the authority + // is server based, i.e. [userinfo@]server[:port] + // DNS names must be US-ASCII and are case insensitive, so we force all to lowercase + + String host = uri.getHost() == null ? null : uri.getHost().toLowerCase(Locale.ENGLISH); + int port = uri.getPort(); + if ("http".equals(scheme) && port == 80) { + port = -1; + } else if ("https".equals(scheme) && port == 443) { + port = -1; + } + serverUrl = new URI( + scheme, + uri.getUserInfo(), + host, + port, + uri.getPath(), + uri.getQuery(), + uri.getFragment() + ).toASCIIString(); + } + } catch (URISyntaxException e) { + // ignore, this was a best effort tidy-up + } + return serverUrl.replaceAll("/$", ""); } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint.java index c1e64d192..292f8926a 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint.java @@ -32,7 +32,6 @@ import java.net.MalformedURLException; import java.net.URL; import javax.annotation.Nonnull; -import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; @@ -67,7 +66,7 @@ public BitbucketServerEndpoint(@CheckForNull String displayName, @NonNull String @CheckForNull String credentialsId) { super(manageHooks, credentialsId); this.displayName = Util.fixEmpty(displayName); - this.serverUrl = StringUtils.defaultString(serverUrl).replaceAll("/$", ""); + this.serverUrl = BitbucketEndpointConfiguration.normalizeServerUrl(serverUrl); } /** diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfigurationTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfigurationTest.java new file mode 100644 index 000000000..b04c24de3 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfigurationTest.java @@ -0,0 +1,618 @@ +/* + * 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.endpoints; + +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.XmlFile; +import hudson.security.ACL; +import hudson.security.AuthorizationStrategy; +import hudson.security.FullControlOnceLoggedInAuthorizationStrategy; +import hudson.util.ListBoxModel; +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import jenkins.model.Jenkins; +import org.acegisecurity.AccessDeniedException; +import org.acegisecurity.context.SecurityContext; +import org.acegisecurity.context.SecurityContextHolder; +import org.junit.Before; +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.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assume.assumeThat; + +public class BitbucketEndpointConfigurationTest { + @ClassRule + public static JenkinsRule j = new JenkinsRule(); + + @Before + public void cleanUp() { + BitbucketEndpointConfiguration.get().setEndpoints(null); + new XmlFile(new File(Jenkins.getInstance().getRootDir(), BitbucketEndpointConfiguration.get().getId() + ".xml")) + .delete(); + SystemCredentialsProvider.getInstance() + .setDomainCredentialsMap(Collections.>emptyMap()); + } + + @Test + public void given__newInstance__when__notConfigured__then__cloudPresent() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + assertThat(instance.getEndpoints(), contains(instanceOf(BitbucketCloudEndpoint.class))); + } + + @Test + public void given__newInstance__when__configuredWithEmpty__then__cloudPresent() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints(Collections.emptyList()); + assertThat(instance.getEndpoints(), contains(instanceOf(BitbucketCloudEndpoint.class))); + } + + @Test + public void given__newInstance__when__configuredWithCloud__then__cloudPresent() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + assumeThat(instance.getEndpoints().get(0).getCredentialsId(), not(is("dummy"))); + instance.setEndpoints( + Collections.singletonList(new BitbucketCloudEndpoint(true, "dummy"))); + assertThat(instance.getEndpoints(), contains(instanceOf(BitbucketCloudEndpoint.class))); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is("dummy")); + } + + @Test + public void given__newInstance__when__configuredWithMultipleCloud__then__onlyFirstCloudPresent() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + assumeThat(instance.getEndpoints().get(0).getCredentialsId(), not(is("first"))); + instance.setEndpoints(Arrays.asList(new BitbucketCloudEndpoint(true, "first"), + new BitbucketCloudEndpoint(true, "second"), new BitbucketCloudEndpoint(true, "third"))); + assertThat(instance.getEndpoints(), contains(instanceOf(BitbucketCloudEndpoint.class))); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is("first")); + } + + @Test(expected = AccessDeniedException.class) + public void given__newInstance__when__configuredAsAnon__then__permissionError() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + j.jenkins.setAuthorizationStrategy(new FullControlOnceLoggedInAuthorizationStrategy()); + SecurityContext ctx = ACL.impersonate(Jenkins.ANONYMOUS); + try { + instance.setEndpoints(Arrays.asList(new BitbucketCloudEndpoint(true, "first"), + new BitbucketCloudEndpoint(true, "second"), new BitbucketCloudEndpoint(true, "third"))); + assertThat(instance.getEndpoints(), contains(instanceOf(BitbucketCloudEndpoint.class))); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is("first")); + } finally { + SecurityContextHolder.setContext(ctx); + j.jenkins.setAuthorizationStrategy(AuthorizationStrategy.UNSECURED); + } + } + + @Test + public void given__newInstance__when__configuredWithServerUsingCloudUrl__then__convertedToCloud() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + assumeThat(instance.getEndpoints().get(0).getCredentialsId(), not(is("dummy"))); + instance.setEndpoints(Arrays.asList( + new BitbucketServerEndpoint("I am silly", BitbucketCloudEndpoint.SERVER_URL, true, "dummy"), + new BitbucketCloudEndpoint(true, "second"), new BitbucketCloudEndpoint(true, "third"))); + assertThat(instance.getEndpoints(), contains(instanceOf(BitbucketCloudEndpoint.class))); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is("dummy")); + } + + @Test + public void given__newInstance__when__configuredWithServer__then__serverPresent() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + assumeThat(instance.getEndpoints().get(0).getCredentialsId(), not(is("dummy"))); + instance.setEndpoints( + Collections.singletonList( + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "dummy"))); + assertThat(instance.getEndpoints(), contains(instanceOf(BitbucketServerEndpoint.class))); + assertThat(instance.getEndpoints().get(0).getDisplayName(), is("Example Inc")); + assertThat(instance.getEndpoints().get(0).getServerUrl(), is("https://bitbucket.example.com")); + assertThat(instance.getEndpoints().get(0).isManageHooks(), is(true)); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is("dummy")); + } + + @Test + public void given__newInstance__when__configuredWithTwoServers__then__serversPresent() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + assumeThat(instance.getEndpoints().get(0).getCredentialsId(), not(is("dummy"))); + instance.setEndpoints( + Arrays.asList( + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "dummy"), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", false, null))); + assertThat(instance.getEndpoints(), + contains(instanceOf(BitbucketServerEndpoint.class), instanceOf(BitbucketServerEndpoint.class))); + assertThat(instance.getEndpoints().get(0).getDisplayName(), is("Example Inc")); + assertThat(instance.getEndpoints().get(0).getServerUrl(), is("https://bitbucket.example.com")); + assertThat(instance.getEndpoints().get(0).isManageHooks(), is(true)); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is("dummy")); + assertThat(instance.getEndpoints().get(1).getDisplayName(), is("Example Org")); + assertThat(instance.getEndpoints().get(1).getServerUrl(), is("http://example.org:8080/bitbucket")); + assertThat(instance.getEndpoints().get(1).isManageHooks(), is(false)); + assertThat(instance.getEndpoints().get(1).getCredentialsId(), is(nullValue())); + } + + @Test + public void given__instanceWithCloud__when__addingAnotherCloud__then__onlyFirstCloudRetained() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints( + Collections.singletonList(new BitbucketCloudEndpoint(true, "dummy"))); + assumeThat(instance.getEndpoints(), contains(instanceOf(BitbucketCloudEndpoint.class))); + assumeThat(instance.getEndpoints().get(0).getCredentialsId(), is("dummy")); + assertThat(instance.addEndpoint(new BitbucketCloudEndpoint(false, null)), is(false)); + assertThat(instance.getEndpoints(), contains(instanceOf(BitbucketCloudEndpoint.class))); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is("dummy")); + } + + @Test + public void given__instanceWithServer__when__addingCloud__then__cloudAdded() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints( + Collections.singletonList( + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "dummy"))); + assumeThat(instance.getEndpoints(), contains(instanceOf(BitbucketServerEndpoint.class))); + assumeThat(instance.getEndpoints().get(0).getCredentialsId(), is("dummy")); + assertThat(instance.addEndpoint(new BitbucketCloudEndpoint(true, "added")), is(true)); + assertThat(instance.getEndpoints(), + contains(instanceOf(BitbucketServerEndpoint.class), instanceOf(BitbucketCloudEndpoint.class))); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is("dummy")); + assertThat(instance.getEndpoints().get(1).getCredentialsId(), is("added")); + } + + @Test + public void given__instanceWithServer__when__addingDifferentServer__then__serverAdded() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints( + Collections.singletonList( + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "dummy"))); + assumeThat(instance.getEndpoints(), contains(instanceOf(BitbucketServerEndpoint.class))); + assumeThat(instance.getEndpoints().get(0).getCredentialsId(), is("dummy")); + assertThat(instance.addEndpoint( + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "added")), + is(true)); + assertThat(instance.getEndpoints(), contains(instanceOf(BitbucketServerEndpoint.class), instanceOf( + BitbucketServerEndpoint.class))); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is("dummy")); + assertThat(instance.getEndpoints().get(1).getCredentialsId(), is("added")); + } + + @Test + public void given__instanceWithServer__when__addingSameServer__then__onlyFirstServerRetained() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints( + Collections.singletonList( + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "dummy"))); + assumeThat(instance.getEndpoints(), contains(instanceOf(BitbucketServerEndpoint.class))); + assumeThat(instance.getEndpoints().get(0).getCredentialsId(), is("dummy")); + assertThat(instance.addEndpoint( + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", false, null)), is(false)); + assertThat(instance.getEndpoints(), contains(instanceOf(BitbucketServerEndpoint.class))); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is("dummy")); + } + + @Test + public void given__instanceWithCloud__when__updatingCloud__then__cloudUpdated() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints( + Collections.singletonList(new BitbucketCloudEndpoint(true, "dummy"))); + assumeThat(instance.getEndpoints(), contains(instanceOf(BitbucketCloudEndpoint.class))); + assumeThat(instance.getEndpoints().get(0).getCredentialsId(), is("dummy")); + instance.updateEndpoint(new BitbucketCloudEndpoint(false, null)); + assertThat(instance.getEndpoints(), contains(instanceOf(BitbucketCloudEndpoint.class))); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is(nullValue())); + } + + @Test + public void given__instanceWithServer__when__updatingCloud__then__cloudAdded() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints( + Collections.singletonList( + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "dummy"))); + assumeThat(instance.getEndpoints(), contains(instanceOf(BitbucketServerEndpoint.class))); + assumeThat(instance.getEndpoints().get(0).getCredentialsId(), is("dummy")); + instance.updateEndpoint(new BitbucketCloudEndpoint(true, "added")); + assertThat(instance.getEndpoints(), + contains(instanceOf(BitbucketServerEndpoint.class), instanceOf(BitbucketCloudEndpoint.class))); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is("dummy")); + assertThat(instance.getEndpoints().get(1).getCredentialsId(), is("added")); + } + + @Test + public void given__instanceWithServer__when__updatingDifferentServer__then__serverAdded() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints( + Collections.singletonList( + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "dummy"))); + assumeThat(instance.getEndpoints(), contains(instanceOf(BitbucketServerEndpoint.class))); + assumeThat(instance.getEndpoints().get(0).getCredentialsId(), is("dummy")); + instance.updateEndpoint( + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "added")); + assertThat(instance.getEndpoints(), contains(instanceOf(BitbucketServerEndpoint.class), instanceOf( + BitbucketServerEndpoint.class))); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is("dummy")); + assertThat(instance.getEndpoints().get(1).getCredentialsId(), is("added")); + } + + @Test + public void given__instanceWithServer__when__updatingSameServer__then__serverUpdated() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints( + Collections.singletonList( + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "dummy"))); + assumeThat(instance.getEndpoints(), contains(instanceOf(BitbucketServerEndpoint.class))); + assertThat(instance.getEndpoints().get(0).getDisplayName(), is("Example Inc")); + assertThat(instance.getEndpoints().get(0).getServerUrl(), is("https://bitbucket.example.com")); + assertThat(instance.getEndpoints().get(0).isManageHooks(), is(true)); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is("dummy")); + instance.updateEndpoint( + new BitbucketServerEndpoint("Example, Inc.", "https://bitbucket.example.com/", false, null)); + assertThat(instance.getEndpoints(), contains(instanceOf(BitbucketServerEndpoint.class))); + assertThat(instance.getEndpoints().get(0).getDisplayName(), is("Example, Inc.")); + assertThat(instance.getEndpoints().get(0).getServerUrl(), is("https://bitbucket.example.com")); + assertThat(instance.getEndpoints().get(0).isManageHooks(), is(false)); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is(nullValue())); + } + + @Test + public void given__newInstance__when__removingCloud__then__defaultRestored() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + assumeThat(instance.getEndpoints(), contains(instanceOf(BitbucketCloudEndpoint.class))); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is(nullValue())); + assertThat(instance.removeEndpoint(new BitbucketCloudEndpoint(true, "dummy")), is(true)); + assertThat(instance.getEndpoints(), contains(instanceOf(BitbucketCloudEndpoint.class))); + } + + @Test + public void given__instanceWithCloudAndServers__when__removingServer__then__matchingServerRemoved() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints( + Arrays.asList( + new BitbucketCloudEndpoint(true, "first"), + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "second"), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "third") + )); + assumeThat(instance.getEndpoints(), + contains(instanceOf(BitbucketCloudEndpoint.class), instanceOf(BitbucketServerEndpoint.class), + instanceOf(BitbucketServerEndpoint.class))); + assumeThat(instance.getEndpoints().get(0).getCredentialsId(), is("first")); + assumeThat(instance.getEndpoints().get(1).getCredentialsId(), is("second")); + assumeThat(instance.getEndpoints().get(2).getCredentialsId(), is("third")); + assertThat(instance.removeEndpoint( + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", false, null)), is(true)); + assertThat(instance.getEndpoints(), + contains(instanceOf(BitbucketCloudEndpoint.class), instanceOf(BitbucketServerEndpoint.class))); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is("first")); + assertThat(instance.getEndpoints().get(1).getCredentialsId(), is("third")); + } + + @Test + public void given__instanceWithCloudAndServers__when__removingCloud__then__cloudRemoved() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints( + Arrays.asList( + new BitbucketCloudEndpoint(true, "first"), + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "second"), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "third") + )); + assumeThat(instance.getEndpoints(), + contains(instanceOf(BitbucketCloudEndpoint.class), instanceOf(BitbucketServerEndpoint.class), + instanceOf(BitbucketServerEndpoint.class))); + assumeThat(instance.getEndpoints().get(0).getCredentialsId(), is("first")); + assumeThat(instance.getEndpoints().get(1).getCredentialsId(), is("second")); + assumeThat(instance.getEndpoints().get(2).getCredentialsId(), is("third")); + assertThat(instance.removeEndpoint( + new BitbucketCloudEndpoint(false, null)), is(true)); + assertThat(instance.getEndpoints(), + contains(instanceOf(BitbucketServerEndpoint.class), instanceOf(BitbucketServerEndpoint.class))); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is("second")); + assertThat(instance.getEndpoints().get(1).getCredentialsId(), is("third")); + } + + @Test + public void given__instanceWithCloudAndServers__when__removingNonExisting__then__noChange() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints( + Arrays.asList( + new BitbucketCloudEndpoint(true, "first"), + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "second"), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "third") + )); + assumeThat(instance.getEndpoints(), + contains(instanceOf(BitbucketCloudEndpoint.class), instanceOf(BitbucketServerEndpoint.class), + instanceOf(BitbucketServerEndpoint.class))); + assumeThat(instance.getEndpoints().get(0).getCredentialsId(), is("first")); + assumeThat(instance.getEndpoints().get(1).getCredentialsId(), is("second")); + assumeThat(instance.getEndpoints().get(2).getCredentialsId(), is("third")); + assertThat(instance.removeEndpoint( + new BitbucketServerEndpoint("Test", "http://bitbucket.test", true, "fourth")), is(false)); + assertThat(instance.getEndpoints(), + contains(instanceOf(BitbucketCloudEndpoint.class), instanceOf(BitbucketServerEndpoint.class), + instanceOf(BitbucketServerEndpoint.class))); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is("first")); + assertThat(instance.getEndpoints().get(1).getCredentialsId(), is("second")); + assertThat(instance.getEndpoints().get(2).getCredentialsId(), is("third")); + } + + @Test + public void given__instance__when__onlyOneEndpoint__then__endpointsNotSelectable() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints( + Collections.singletonList( + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "dummy") + )); + assertThat(instance.isEndpointSelectable(), is(false)); + } + + @Test + public void given__instance__when__multipleEndpoints__then__endpointsSelectable() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints( + Arrays.asList( + new BitbucketCloudEndpoint(true, "first"), + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "second"), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "third") + )); + assertThat(instance.isEndpointSelectable(), is(true)); + } + + @Test + public void given__instanceWithCloudAndServers__when__findingExistingEndpoint__then__endpointFound() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints( + Arrays.asList( + new BitbucketCloudEndpoint(true, "first"), + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "second"), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "third") + )); + AbstractBitbucketEndpoint ep = instance.findEndpoint(BitbucketCloudEndpoint.SERVER_URL); + assertThat(ep, notNullValue()); + assertThat(ep.getCredentialsId(), is("first")); + + ep = instance.findEndpoint("https://bitbucket.example.com/"); + assertThat(ep, notNullValue()); + assertThat(ep.getCredentialsId(), is("second")); + + ep = instance.findEndpoint("https://bitbucket.example.com"); + assertThat(ep, notNullValue()); + assertThat(ep.getCredentialsId(), is("second")); + + ep = instance.findEndpoint("https://BITBUCKET.EXAMPLE.COM:443/"); + assertThat(ep, notNullValue()); + assertThat(ep.getCredentialsId(), is("second")); + + ep = instance.findEndpoint("http://example.org:8080/bitbucket/../bitbucket/"); + assertThat(ep, notNullValue()); + assertThat(ep.getCredentialsId(), is("third")); + } + + @Test + public void given__instanceWithServers__when__findingNonExistingEndpoint__then__endpointNotFound() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints( + Arrays.asList( + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "dummy"), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "dummy") + )); + assertThat(instance.findEndpoint(BitbucketCloudEndpoint.SERVER_URL), nullValue()); + assertThat(instance.findEndpoint("http://bitbucket.example.com/"), nullValue()); + assertThat(instance.findEndpoint("http://bitbucket.example.com:80/"), nullValue()); + assertThat(instance.findEndpoint("http://bitbucket.example.com:443"), nullValue()); + assertThat(instance.findEndpoint("https://BITBUCKET.EXAMPLE.COM:443/bitbucket/"), nullValue()); + assertThat(instance.findEndpoint("http://example.org/bitbucket/../bitbucket/"), nullValue()); + assertThat(instance.findEndpoint("bitbucket.org"), nullValue()); + assertThat(instance.findEndpoint("bitbucket.example.com"), nullValue()); + } + + @Test + public void given__instanceWithCloudAndServers__when__findingInvalid__then__endpointNotFound() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints( + Arrays.asList( + new BitbucketCloudEndpoint(true, "first"), + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "second"), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "third") + )); + assertThat(instance.findEndpoint("0schemes-start-with+digits:no leading slash"), nullValue()); + assertThat(instance.findEndpoint("http://host name with spaces:443"), nullValue()); + assertThat(instance.findEndpoint("http://invalid.port.test:65536/bitbucket/"), nullValue()); + } + + @Test + public void given__instanceWithCloudAndServers__when__populatingDropBox__then__endpointsListed() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints( + Arrays.asList( + new BitbucketCloudEndpoint(true, "first"), + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "second"), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "third") + )); + ListBoxModel items = instance.getEndpointItems(); + assertThat(items, hasSize(3)); + assertThat(items.get(0).name, is(Messages.BitbucketCloudEndpoint_displayName() + " (" + + BitbucketCloudEndpoint.SERVER_URL + ")")); + assertThat(items.get(0).value, is(BitbucketCloudEndpoint.SERVER_URL)); + assertThat(items.get(1).name, is("Example Inc (https://bitbucket.example.com)")); + assertThat(items.get(1).value, is("https://bitbucket.example.com")); + assertThat(items.get(2).name, is("Example Org (http://example.org:8080/bitbucket)")); + assertThat(items.get(2).value, is("http://example.org:8080/bitbucket")); + } + + @Test + public void given__instanceWithCloudAndServers__when__resolvingExistingEndpoint__then__normalizedReturned() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints( + Arrays.asList( + new BitbucketCloudEndpoint(true, "first"), + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "second"), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "third") + )); + assertThat(instance.getEndpointItems(), hasSize(3)); + assertThat(instance.readResolveServerUrl(null), is(BitbucketCloudEndpoint.SERVER_URL)); + assertThat(instance.getEndpointItems(), hasSize(3)); + assertThat(instance.readResolveServerUrl(""), is(BitbucketCloudEndpoint.SERVER_URL)); + assertThat(instance.getEndpointItems(), hasSize(3)); + assertThat(instance.readResolveServerUrl("https://bitbucket.EXAMPLE.COM:443/"), + is("https://bitbucket.example.com")); + assertThat(instance.getEndpointItems(), hasSize(3)); + assertThat(instance.readResolveServerUrl("https://bitbucket.example.com"), + is("https://bitbucket.example.com")); + assertThat(instance.getEndpointItems(), hasSize(3)); + assertThat(instance.readResolveServerUrl("http://example.org:8080/bitbucket/"), + is("http://example.org:8080/bitbucket")); + assertThat(instance.getEndpointItems(), hasSize(3)); + assertThat(instance.readResolveServerUrl("http://example.org:8080/bitbucket/foo/../"), + is("http://example.org:8080/bitbucket")); + assertThat(instance.getEndpointItems(), hasSize(3)); + assertThat(instance.readResolveServerUrl("http://example.org:8080/foo/../bitbucket/."), + is("http://example.org:8080/bitbucket")); + assertThat(instance.getEndpointItems(), hasSize(3)); + } + + @Test + public void + given__instanceWithCloudAndServers__when__resolvingNewEndpointAsSystem__then__addedAndNormalizedReturned() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints(Collections.singletonList( + new BitbucketServerEndpoint("existing", "https://bitbucket.test", false, null))); + assertThat(instance.getEndpointItems(), hasSize(1)); + assertThat(instance.readResolveServerUrl(null), is(BitbucketCloudEndpoint.SERVER_URL)); + assertThat(instance.getEndpointItems(), hasSize(2)); + assertThat(instance.readResolveServerUrl(""), is(BitbucketCloudEndpoint.SERVER_URL)); + assertThat(instance.getEndpointItems(), hasSize(2)); + assertThat(instance.readResolveServerUrl("https://bitbucket.EXAMPLE.COM:443/"), + is("https://bitbucket.example.com")); + assertThat(instance.getEndpointItems(), hasSize(3)); + assertThat(instance.readResolveServerUrl("https://bitbucket.example.com"), + is("https://bitbucket.example.com")); + assertThat(instance.getEndpointItems(), hasSize(3)); + assertThat(instance.readResolveServerUrl("http://example.org:8080/bitbucket/"), + is("http://example.org:8080/bitbucket")); + assertThat(instance.getEndpointItems(), hasSize(4)); + assertThat(instance.readResolveServerUrl("http://example.org:8080/bitbucket/foo/../"), + is("http://example.org:8080/bitbucket")); + assertThat(instance.getEndpointItems(), hasSize(4)); + assertThat(instance.readResolveServerUrl("http://example.org:8080/foo/../bitbucket/."), + is("http://example.org:8080/bitbucket")); + assertThat(instance.getEndpointItems(), hasSize(4)); + assertThat(instance.getEndpoints().get(0).getDisplayName(), is("existing")); + assertThat(instance.getEndpoints().get(0).getServerUrl(), is("https://bitbucket.test")); + assertThat(instance.getEndpoints().get(0).isManageHooks(), is(false)); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is(nullValue())); + assertThat(instance.getEndpoints().get(1).getDisplayName(), is(Messages.BitbucketCloudEndpoint_displayName())); + assertThat(instance.getEndpoints().get(1).getServerUrl(), is("https://bitbucket.org")); + assertThat(instance.getEndpoints().get(1).isManageHooks(), is(false)); + assertThat(instance.getEndpoints().get(1).getCredentialsId(), is(nullValue())); + assertThat(instance.getEndpoints().get(2).getDisplayName(), is(nullValue())); + assertThat(instance.getEndpoints().get(2).getServerUrl(), is("https://bitbucket.example.com")); + assertThat(instance.getEndpoints().get(2).isManageHooks(), is(false)); + assertThat(instance.getEndpoints().get(2).getCredentialsId(), is(nullValue())); + assertThat(instance.getEndpoints().get(3).getDisplayName(), is(nullValue())); + assertThat(instance.getEndpoints().get(3).getServerUrl(), is("http://example.org:8080/bitbucket")); + assertThat(instance.getEndpoints().get(3).isManageHooks(), is(false)); + assertThat(instance.getEndpoints().get(3).getCredentialsId(), is(nullValue())); + } + + @Test + public void + given__instanceWithCloudAndServers__when__resolvingNewEndpointAsAnon__then__normalizedReturnedNotAdded() { + BitbucketEndpointConfiguration instance = new BitbucketEndpointConfiguration(); + instance.setEndpoints(Collections.singletonList( + new BitbucketServerEndpoint("existing", "https://bitbucket.test", false, null))); + SecurityContext ctx = ACL.impersonate(Jenkins.ANONYMOUS); + try { + assertThat(instance.getEndpointItems(), hasSize(1)); + assertThat(instance.readResolveServerUrl(null), is(BitbucketCloudEndpoint.SERVER_URL)); + assertThat(instance.getEndpointItems(), hasSize(1)); + assertThat(instance.readResolveServerUrl(""), is(BitbucketCloudEndpoint.SERVER_URL)); + assertThat(instance.getEndpointItems(), hasSize(1)); + assertThat(instance.readResolveServerUrl("https://bitbucket.EXAMPLE.COM:443/"), + is("https://bitbucket.example.com")); + assertThat(instance.getEndpointItems(), hasSize(1)); + assertThat(instance.readResolveServerUrl("https://bitbucket.example.com"), + is("https://bitbucket.example.com")); + assertThat(instance.getEndpointItems(), hasSize(1)); + assertThat(instance.readResolveServerUrl("http://example.org:8080/bitbucket/"), + is("http://example.org:8080/bitbucket")); + assertThat(instance.getEndpointItems(), hasSize(1)); + assertThat(instance.readResolveServerUrl("http://example.org:8080/bitbucket/foo/../"), + is("http://example.org:8080/bitbucket")); + assertThat(instance.getEndpointItems(), hasSize(1)); + assertThat(instance.readResolveServerUrl("http://example.org:8080/foo/../bitbucket/."), + is("http://example.org:8080/bitbucket")); + assertThat(instance.getEndpointItems(), hasSize(1)); + assertThat(instance.getEndpoints().get(0).getDisplayName(), + is("existing")); + assertThat(instance.getEndpoints().get(0).getServerUrl(), is("https://bitbucket.test")); + assertThat(instance.getEndpoints().get(0).isManageHooks(), is(false)); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is(nullValue())); + } finally { + SecurityContextHolder.setContext(ctx); + } + } + + @Test + public void given__instanceWithConfig__when__configRoundtrip__then__configRetained() throws Exception { + BitbucketEndpointConfiguration instance = BitbucketEndpointConfiguration.get(); + instance.setEndpoints( + Arrays.asList( + new BitbucketCloudEndpoint(true, "first"), + new BitbucketServerEndpoint("Example Inc", "https://bitbucket.example.com/", true, "second"), + new BitbucketServerEndpoint("Example Org", "http://example.org:8080/bitbucket/", true, "third") + )); + SystemCredentialsProvider.getInstance().setDomainCredentialsMap( + Collections.singletonMap(Domain.global(), Arrays.asList( + new UsernamePasswordCredentialsImpl( + CredentialsScope.SYSTEM, "first", null, "user1", "pass1"), + new UsernamePasswordCredentialsImpl( + CredentialsScope.SYSTEM, "second", null, "user2", "pass2"), + new UsernamePasswordCredentialsImpl( + CredentialsScope.SYSTEM, "third", null, "user3", "pass3") + ))); + j.configRoundtrip(); + assertThat(instance.getEndpoints(), hasSize(3)); + assertThat(instance.getEndpoints().get(0).getDisplayName(), is(Messages.BitbucketCloudEndpoint_displayName())); + assertThat(instance.getEndpoints().get(0).getServerUrl(), is("https://bitbucket.org")); + assertThat(instance.getEndpoints().get(0).isManageHooks(), is(true)); + assertThat(instance.getEndpoints().get(0).getCredentialsId(), is("first")); + assertThat(instance.getEndpoints().get(1).getDisplayName(), is("Example Inc")); + assertThat(instance.getEndpoints().get(1).getServerUrl(), is("https://bitbucket.example.com")); + assertThat(instance.getEndpoints().get(1).isManageHooks(), is(true)); + assertThat(instance.getEndpoints().get(1).getCredentialsId(), is("second")); + assertThat(instance.getEndpoints().get(2).getDisplayName(), is("Example Org")); + assertThat(instance.getEndpoints().get(2).getServerUrl(), is("http://example.org:8080/bitbucket")); + assertThat(instance.getEndpoints().get(2).isManageHooks(), is(true)); + assertThat(instance.getEndpoints().get(2).getCredentialsId(), is("third")); + } + +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpointTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpointTest.java index f403a1bac..2a6375e91 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpointTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpointTest.java @@ -23,6 +23,7 @@ */ package com.cloudbees.jenkins.plugins.bitbucket.endpoints; +import hudson.util.FormValidation; import org.junit.Test; import static org.hamcrest.Matchers.is; @@ -38,4 +39,17 @@ public void smokes() { assertThat(new BitbucketServerEndpoint("Dummy", "http://dummy.example.com", false, null).getServerUrl(), is( "http://dummy.example.com")); } + + @Test + public void given__badUrl__when__check__then__fail() { + BitbucketServerEndpoint.DescriptorImpl descriptor = new BitbucketServerEndpoint.DescriptorImpl(); + assertThat(BitbucketServerEndpoint.DescriptorImpl.doCheckServerUrl("").kind, is(FormValidation.Kind.ERROR)); + } + + @Test + public void given__goodUrl__when__check__then__ok() { + BitbucketServerEndpoint.DescriptorImpl descriptor = new BitbucketServerEndpoint.DescriptorImpl(); + assertThat(BitbucketServerEndpoint.DescriptorImpl.doCheckServerUrl("http://bitbucket.example.com").kind, + is(FormValidation.Kind.OK)); + } } From ee67e57e5085016813950d65334272099199f34c Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Fri, 2 Jun 2017 13:58:28 +0100 Subject: [PATCH 06/40] [JENKINS-43507] Tidy up implementation debt in BitbucketApi implementations --- .../client/BitbucketCloudApiClient.java | 99 +++++++++--- .../client/BitbucketServerAPIClient.java | 153 ++++++++++++------ 2 files changed, 185 insertions(+), 67 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java index 3d83d0b5e..29d802368 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java @@ -44,45 +44,43 @@ import com.cloudbees.jenkins.plugins.bitbucket.client.repository.BitbucketRepositoryHooks; import com.cloudbees.jenkins.plugins.bitbucket.client.repository.PaginatedBitbucketRepository; import com.cloudbees.jenkins.plugins.bitbucket.client.repository.UserRoleInRepository; -import com.cloudbees.jenkins.plugins.bitbucket.hooks.BitbucketSCMSourcePushHookReceiver; -import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.ProxyConfiguration; +import hudson.Util; import hudson.util.Secret; +import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; import java.net.Proxy; -import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.logging.Logger; import jenkins.model.Jenkins; import net.sf.json.JSONObject; +import org.apache.commons.httpclient.Header; import org.apache.commons.httpclient.HostConfiguration; import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.HttpMethod; import org.apache.commons.httpclient.HttpState; +import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; import org.apache.commons.httpclient.NameValuePair; -import org.apache.commons.httpclient.UsernamePasswordCredentials; import org.apache.commons.httpclient.URIException; +import org.apache.commons.httpclient.UsernamePasswordCredentials; import org.apache.commons.httpclient.auth.AuthScope; import org.apache.commons.httpclient.methods.DeleteMethod; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.methods.StringRequestEntity; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; -import org.codehaus.jackson.JsonGenerationException; import org.codehaus.jackson.JsonNode; -import org.codehaus.jackson.JsonParseException; -import org.codehaus.jackson.map.JsonMappingException; import org.codehaus.jackson.map.ObjectMapper; public class BitbucketCloudApiClient implements BitbucketApi { @@ -272,7 +270,17 @@ public void deletePullRequestApproval(String pullRequestId) throws IOException, */ @Override public boolean checkPathExists(@NonNull String branch, @NonNull String path) throws IOException, InterruptedException { - int status = getRequestStatus(V1_API_BASE_URL + owner + "/" + repositoryName + "/raw/" + branch + "/" + path); + StringBuilder url = new StringBuilder(V1_API_BASE_URL); + url.append(owner); + url.append('/'); + url.append(repositoryName); + url.append("/raw/"); + url.append(Util.rawEncode(branch)); + for (String segment : StringUtils.split(path, "/")) { + url.append('/'); + url.append(Util.rawEncode(segment)); + } + int status = getRequestStatus(url.toString()); return status == HttpStatus.SC_OK; } @@ -360,7 +368,7 @@ public void removeCommitWebHook(@NonNull BitbucketWebHook hook) throws IOExcepti if (StringUtils.isBlank(hook.getUuid())) { throw new BitbucketException("Hook UUID required"); } - deleteRequest(V2_API_BASE_URL + owner + "/" + repositoryName + "/hooks/" + URLEncoder.encode(hook.getUuid(), "UTF-8")); + deleteRequest(V2_API_BASE_URL + owner + "/" + repositoryName + "/hooks/" + Util.rawEncode(hook.getUuid())); } /** @@ -397,7 +405,9 @@ public List getWebHooks() throws IOException, Interrupt */ @Override public void postBuildStatus(@NonNull BitbucketBuildStatus status) throws IOException { - String path = V2_API_BASE_URL + this.owner + "/" + this.repositoryName + "/commit/" + status.getHash() + "/statuses/build";; + String path = V2_API_BASE_URL + this.owner + "/" + this.repositoryName + "/commit/" + status.getHash() + + "/statuses/build"; + postRequest(path, serialize(status)); } @@ -409,14 +419,15 @@ public boolean isPrivate() throws IOException, InterruptedException { return getRepository().isPrivate(); } - private BitbucketRepositoryHooks parsePaginatedRepositoryHooks(String response) throws JsonParseException, JsonMappingException, IOException { + private BitbucketRepositoryHooks parsePaginatedRepositoryHooks(String response) throws + IOException { ObjectMapper mapper = new ObjectMapper(); BitbucketRepositoryHooks parsedResponse; parsedResponse = mapper.readValue(response, BitbucketRepositoryHooks.class); return parsedResponse; } - private String asJson(BitbucketWebHook hook) throws JsonGenerationException, JsonMappingException, IOException { + private String asJson(BitbucketWebHook hook) throws IOException { ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(hook); } @@ -430,6 +441,8 @@ public BitbucketTeam getTeam() throws IOException, InterruptedException { try { String response = getRequest(V2_TEAMS_API_BASE_URL + owner); return parse(response, BitbucketCloudTeam.class); + } catch (FileNotFoundException e) { + return null; } catch (IOException e) { throw new IOException("I/O error when parsing response from URL: " + V2_TEAMS_API_BASE_URL + owner, e); @@ -487,8 +500,12 @@ private synchronized HttpClient getHttpClient() { client.getParams().setConnectionManagerTimeout(10 * 1000); client.getParams().setSoTimeout(60 * 1000); - client.getState().setCredentials(AuthScope.ANY, credentials); - client.getParams().setAuthenticationPreemptive(true); + if (credentials != null) { + client.getState().setCredentials(AuthScope.ANY, credentials); + client.getParams().setAuthenticationPreemptive(true); + } else { + client.getParams().setAuthenticationPreemptive(false); + } setClientProxyParams("bitbucket.org", client); this.client = client; @@ -563,7 +580,22 @@ private String getRequest(String path) throws IOException, InterruptedException GetMethod httpget = new GetMethod(path); try { executeMethod(client, httpget); - String response = new String(httpget.getResponseBody(), "UTF-8"); + String response; + long len = httpget.getResponseContentLength(); + if (len == 0) { + response = ""; + } else { + ByteArrayOutputStream buf; + if (len > 0 && len <= Integer.MAX_VALUE / 2) { + buf = new ByteArrayOutputStream((int) len); + } else { + buf = new ByteArrayOutputStream(); + } + try (InputStream is = httpget.getResponseBodyAsStream()) { + IOUtils.copy(is, buf); + } + response = new String(buf.toByteArray(), StandardCharsets.UTF_8); + } if (httpget.getStatusCode() == HttpStatus.SC_NOT_FOUND) { throw new FileNotFoundException("URL: " + path); } @@ -631,7 +663,36 @@ private String postRequest(PostMethod httppost) throws IOException { // 204, no content return ""; } - String response = new String(httppost.getResponseBody(), "UTF-8"); + String response; + long len = -1L; + Header[] headers = httppost.getResponseHeaders("Content-Length"); + if (headers != null && headers.length > 0) { + int i = headers.length - 1; + len = -1L; + while (i >= 0) { + Header header = headers[i]; + try { + len = Long.parseLong(header.getValue()); + break; + } catch (NumberFormatException var5) { + --i; + } + } + } + if (len == 0) { + response = ""; + } else { + ByteArrayOutputStream buf; + if (len > 0 && len <= Integer.MAX_VALUE / 2) { + buf = new ByteArrayOutputStream((int) len); + } else { + buf = new ByteArrayOutputStream(); + } + try (InputStream is = httppost.getResponseBodyAsStream()) { + IOUtils.copy(is, buf); + } + response = new String(buf.toByteArray(), StandardCharsets.UTF_8); + } if (httppost.getStatusCode() != HttpStatus.SC_OK && httppost.getStatusCode() != HttpStatus.SC_CREATED) { throw new BitbucketRequestException(httppost.getStatusCode(), "HTTP request error. Status: " + httppost.getStatusCode() + ": " + httppost.getStatusText() + ".\n" + response); } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java index b2a3802f4..681dbe5c3 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java @@ -23,72 +23,64 @@ */ package com.cloudbees.jenkins.plugins.bitbucket.server.client; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryProtocol; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryType; -import edu.umd.cs.findbugs.annotations.CheckForNull; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.MalformedURLException; -import java.net.Proxy; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; - -import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.HttpStatus; -import org.apache.commons.httpclient.HttpMethod; -import org.apache.commons.httpclient.NameValuePair; -import org.apache.commons.httpclient.UsernamePasswordCredentials; -import org.apache.commons.httpclient.URIException; -import org.apache.commons.httpclient.auth.AuthScope; -import org.apache.commons.httpclient.methods.GetMethod; -import org.apache.commons.httpclient.methods.PostMethod; -import org.apache.commons.httpclient.methods.StringRequestEntity; -import org.codehaus.jackson.map.ObjectMapper; - import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketCommit; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryProtocol; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepositoryType; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRequestException; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketTeam; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook; import com.cloudbees.jenkins.plugins.bitbucket.client.repository.UserRoleInRepository; -import com.cloudbees.jenkins.plugins.bitbucket.hooks.BitbucketSCMSourcePushHookReceiver; import com.cloudbees.jenkins.plugins.bitbucket.server.client.branch.BitbucketServerBranch; import com.cloudbees.jenkins.plugins.bitbucket.server.client.branch.BitbucketServerBranches; import com.cloudbees.jenkins.plugins.bitbucket.server.client.branch.BitbucketServerCommit; import com.cloudbees.jenkins.plugins.bitbucket.server.client.pullrequest.BitbucketServerPullRequest; import com.cloudbees.jenkins.plugins.bitbucket.server.client.pullrequest.BitbucketServerPullRequests; -import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.*; +import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerProject; +import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerRepositories; +import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerRepository; +import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerWebhooks; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; import hudson.ProxyConfiguration; +import hudson.Util; import hudson.util.Secret; -import jenkins.model.Jenkins; -import net.sf.json.JSONObject; -import org.apache.commons.httpclient.*; -import org.apache.commons.httpclient.auth.AuthScope; -import org.apache.commons.httpclient.methods.*; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.StringUtils; -import org.codehaus.jackson.map.ObjectMapper; - +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; +import java.net.MalformedURLException; import java.net.Proxy; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import jenkins.model.Jenkins; +import net.sf.json.JSONObject; +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpMethod; +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.NameValuePair; +import org.apache.commons.httpclient.URIException; +import org.apache.commons.httpclient.UsernamePasswordCredentials; +import org.apache.commons.httpclient.auth.AuthScope; +import org.apache.commons.httpclient.methods.DeleteMethod; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.methods.PutMethod; +import org.apache.commons.httpclient.methods.StringRequestEntity; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.codehaus.jackson.map.ObjectMapper; /** * Bitbucket API client. @@ -119,7 +111,6 @@ public class BitbucketServerAPIClient implements BitbucketApi { /** * Repository owner. - * This must be null if {@link #project} is not null. */ private String owner; @@ -328,7 +319,19 @@ public void postBuildStatus(@NonNull BitbucketBuildStatus status) throws IOExcep */ @Override public boolean checkPathExists(@NonNull String branch, @NonNull String path) throws IOException { - return HttpStatus.SC_OK == getRequestStatus(String.format(API_BROWSE_PATH, getUserCentricOwner(), repositoryName, path, branch)); + StringBuilder encodedPath = new StringBuilder(path.length() + 10); + boolean first = true; + for (String segment : StringUtils.split(path, "/")) { + if (first) { + first = false; + } else { + encodedPath.append('/'); + } + encodedPath.append(Util.rawEncode(segment)); + } + int status = getRequestStatus(String.format(API_BROWSE_PATH, getUserCentricOwner(), repositoryName, encodedPath, + URLEncoder.encode(branch, "UTF-8"))); + return HttpStatus.SC_OK == status; } @NonNull @@ -421,6 +424,8 @@ public BitbucketTeam getTeam() throws IOException { try { String response = getRequest(url); return parse(response, BitbucketServerProject.class); + } catch (FileNotFoundException e) { + return null; } catch (IOException e) { throw new IOException("I/O error when accessing URL: " + url, e); } @@ -453,6 +458,8 @@ public List getRepositories(@CheckForNull UserRoleInR repositories.addAll(page.getValues()); } return repositories; + } catch (FileNotFoundException e) { + return new ArrayList<>(); } catch (IOException e) { throw new IOException("I/O error when accessing URL: " + url, e); } @@ -481,12 +488,29 @@ private String getRequest(String path) throws IOException { HttpClient client = getHttpClient(getMethodHost(httpget)); try { client.executeMethod(httpget); - String response = new String(httpget.getResponseBody(), "UTF-8"); + String response; + long len = httpget.getResponseContentLength(); + if (len == 0) { + response = ""; + } else { + ByteArrayOutputStream buf; + if (len > 0 && len <= Integer.MAX_VALUE / 2) { + buf = new ByteArrayOutputStream((int) len); + } else { + buf = new ByteArrayOutputStream(); + } + try (InputStream is = httpget.getResponseBodyAsStream()) { + IOUtils.copy(is, buf); + } + response = new String(buf.toByteArray(), StandardCharsets.UTF_8); + } if (httpget.getStatusCode() == HttpStatus.SC_NOT_FOUND) { throw new FileNotFoundException("URL: " + path); } if (httpget.getStatusCode() != HttpStatus.SC_OK) { - throw new BitbucketRequestException(httpget.getStatusCode(), "HTTP request error. Status: " + httpget.getStatusCode() + ": " + httpget.getStatusText() + ".\n" + response); + throw new BitbucketRequestException(httpget.getStatusCode(), + "HTTP request error. Status: " + httpget.getStatusCode() + + ": " + httpget.getStatusText() + ".\n" + response); } return response; } catch (BitbucketRequestException | FileNotFoundException e) { @@ -504,8 +528,12 @@ private HttpClient getHttpClient(String host) { client.getParams().setConnectionManagerTimeout(10 * 1000); client.getParams().setSoTimeout(60 * 1000); - client.getState().setCredentials(AuthScope.ANY, credentials); - client.getParams().setAuthenticationPreemptive(true); + if (credentials != null) { + client.getState().setCredentials(AuthScope.ANY, credentials); + client.getParams().setAuthenticationPreemptive(true); + } else { + client.getParams().setAuthenticationPreemptive(false); + } setClientProxyParams(host, client); return client; @@ -595,7 +623,36 @@ private String doRequest(HttpMethod httppost) throws IOException { // 204, no content return ""; } - String response = new String(httppost.getResponseBody(), "UTF-8"); + String response; + long len = -1L; + Header[] headers = httppost.getResponseHeaders("Content-Length"); + if (headers != null && headers.length > 0) { + int i = headers.length - 1; + len = -1L; + while (i >= 0) { + Header header = headers[i]; + try { + len = Long.parseLong(header.getValue()); + break; + } catch (NumberFormatException var5) { + --i; + } + } + } + if (len == 0) { + response = ""; + } else { + ByteArrayOutputStream buf; + if (len > 0 && len <= Integer.MAX_VALUE / 2) { + buf = new ByteArrayOutputStream((int) len); + } else { + buf = new ByteArrayOutputStream(); + } + try (InputStream is = httppost.getResponseBodyAsStream()) { + IOUtils.copy(is, buf); + } + response = new String(buf.toByteArray(), StandardCharsets.UTF_8); + } if (httppost.getStatusCode() != HttpStatus.SC_OK && httppost.getStatusCode() != HttpStatus.SC_CREATED) { throw new BitbucketRequestException(httppost.getStatusCode(), "HTTP request error. Status: " + httppost.getStatusCode() + ": " + httppost.getStatusText() + ".\n" + response); } From fd9e115378c3aeb563d6c1cbf6f08a2ca3526f4e Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Fri, 2 Jun 2017 13:59:33 +0100 Subject: [PATCH 07/40] [JENKINS-43507] Align with endpoints expected serverUrls --- .../plugins/bitbucket/client/BitbucketCloudApiFactory.java | 3 ++- .../bitbucket/server/client/BitbucketServerApiFactory.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiFactory.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiFactory.java index 669086daf..1524c80eb 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiFactory.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiFactory.java @@ -2,6 +2,7 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApiFactory; +import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; @@ -12,7 +13,7 @@ public class BitbucketCloudApiFactory extends BitbucketApiFactory { @Override protected boolean isMatch(@Nullable String serverUrl) { - return serverUrl == null; + return serverUrl == null || BitbucketCloudEndpoint.SERVER_URL.equals(serverUrl); } @NonNull diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerApiFactory.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerApiFactory.java index a975647c1..46e1250f1 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerApiFactory.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerApiFactory.java @@ -2,6 +2,7 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApiFactory; +import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; @@ -12,7 +13,7 @@ public class BitbucketServerApiFactory extends BitbucketApiFactory { @Override protected boolean isMatch(@Nullable String serverUrl) { - return serverUrl != null; + return serverUrl != null && !BitbucketCloudEndpoint.SERVER_URL.equals(serverUrl); } @NonNull From 824dfd2cac7cef51da6165abfb63e574d0e550f4 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Fri, 2 Jun 2017 14:02:35 +0100 Subject: [PATCH 08/40] [JENKINS-43507] Add backfill until JENKINS-42443 is available in base Jenkins --- .../plugins/bitbucket/FillErrorResponse.java | 35 ++++++ .../plugins/bitbucket/form/select.jelly | 60 ++++++++++ .../plugins/bitbucket/form/select/select.css | 11 ++ .../plugins/bitbucket/form/select/select.js | 107 ++++++++++++++++++ .../plugins/bitbucket/form/select/spinner.gif | Bin 0 -> 1659 bytes .../jenkins/plugins/bitbucket/form/taglib | 0 6 files changed, 213 insertions(+) create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/FillErrorResponse.java create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/form/select.jelly create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/form/select/select.css create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/form/select/select.js create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/form/select/spinner.gif create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/form/taglib diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/FillErrorResponse.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/FillErrorResponse.java new file mode 100644 index 000000000..e720a7bb6 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/FillErrorResponse.java @@ -0,0 +1,35 @@ +package com.cloudbees.jenkins.plugins.bitbucket; + +import hudson.Util; +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import jenkins.model.Jenkins; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +// TODO replace with corresponding core functionality once Jenkins core has JENKINS-42443 +class FillErrorResponse extends IOException implements HttpResponse { + + private final String message; + private final boolean clearList; + + public FillErrorResponse(String message, boolean clearList) { + this.message = message; + this.clearList = clearList; + } + + @Override + public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) + throws IOException, ServletException { + rsp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + rsp.setContentType("text/html;charset=UTF-8"); + rsp.setHeader("X-Jenkins-Select-Error", clearList ? "clear" : "retain"); + rsp.getWriter().print( + "
" + Util.escape(message) + + "
"); + + } +} diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/form/select.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/form/select.jelly new file mode 100644 index 000000000..ea4de17be --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/form/select.jelly @@ -0,0 +1,60 @@ + + + + + + + Glorified {@code select} control that supports the data binding and AJAX updates. + Your descriptor should have the 'doFillXyzItems' method, which returns a ListBoxModel + representation of the items in your drop-down list box, and your instance field should + hold the current value. + + + Additional CSS classes that the control gets. + + + Used for databinding. + + + The default value of the text box, in case both @value is and 'instance[field]' is null. + + + + + + + ${descriptor.calcFillSettings(field,attrs)} + + + + + + + + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/form/select/select.css b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/form/select/select.css new file mode 100644 index 000000000..22a82e534 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/form/select/select.css @@ -0,0 +1,11 @@ +/* TODO remove once Jenkins core has JENKINS-42443 */ +select.select2-loading { + padding-left: 1.5em; + padding-top: 0.5em; + padding-bottom: 0.5em; + color: transparent; + background-image: url("spinner.gif"); + background-repeat: no-repeat; + background-position: 2px; +} + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/form/select/select.js b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/form/select/select.js new file mode 100644 index 000000000..6daea513d --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/form/select/select.js @@ -0,0 +1,107 @@ +// TODO remove once Jenkins core has JENKINS-42443 +// send async request to the given URL (which will send back serialized ListBoxModel object), +// then use the result to fill the list box. +function updateListBox2(listBox, url, config) { + config = config || {}; + config = object(config); + var originalOnSuccess = config.onSuccess; + var l = $(listBox); + var status = findFollowingTR(listBox, "validation-error-area").firstChild.nextSibling; + if (status.firstChild && status.firstChild.getAttribute('data-select2')) { + status.innerHTML = ""; + } + config.onSuccess = function (rsp) { + l.removeClassName("select2-loading"); + var currentSelection = l.value; + + // clear the contents + while (l.length > 0) { + l.options[0] = null; + } + + var selectionSet = false; // is the selection forced by the server? + var possibleIndex = null; // if there's a new option that matches the current value, remember its index + var opts = eval('(' + rsp.responseText + ')').values; + for (var i = 0; i < opts.length; i++) { + l.options[i] = new Option(opts[i].name, opts[i].value); + if (opts[i].selected) { + l.selectedIndex = i; + selectionSet = true; + } + if (opts[i].value == currentSelection) { + possibleIndex = i; + } + } + + // if no value is explicitly selected by the server, try to select the same value + if (!selectionSet && possibleIndex != null) { + l.selectedIndex = possibleIndex; + } + + if (originalOnSuccess != undefined) { + originalOnSuccess(rsp); + } + }; + config.onFailure = function (rsp) { + l.removeClassName("select2-loading"); + status.innerHTML = rsp.responseText; + if (status.firstChild) { + status.firstChild.setAttribute('data-select2', 'true') + } + Behaviour.applySubtree(status); + // deleting values can result in the data loss, so let's not do that unless instructed + var header = rsp.getResponseHeader('X-Jenkins-Select-Error'); + if (header && "clear".toUpperCase() === header.toUpperCase()) { + for (var i = l.options.length - 1; i >= 0; i--) { + l.remove(i); + } + } + + }; + + l.addClassName("select2-loading"); + new Ajax.Request(url, config); +} + +Behaviour.specify("SELECT.select2", 'select2', 1000, function (e) { + + function hasChanged(selectEl, originalValue) { + var firstValue = selectEl.options[0].value; + var selectedValue = selectEl.value; + if (originalValue == "" && selectedValue == firstValue) { + // There was no value pre-selected but after the call to updateListBox the first value is selected by + // default. This must not be considered a change. + return false; + } else { + return originalValue != selectedValue; + } + } + + // controls that this SELECT box depends on + refillOnChange(e, function (params) { + var value = e.value; + updateListBox2(e, e.getAttribute("fillUrl"), { + parameters: params, + onSuccess: function () { + if (value == "") { + // reflect the initial value. if the control depends on several other SELECT.select, + // it may take several updates before we get the right items, which is why all these precautions. + var v = e.getAttribute("value"); + if (v) { + e.value = v; + if (e.value == v) { + e.removeAttribute("value"); + } // we were able to apply our initial value + } + } + + fireEvent(e, "filled"); // let other interested parties know that the items have changed + + // if the update changed the current selection, others listening to this control needs to be notified. + if (hasChanged(e, value)) { + fireEvent(e, "change"); + } + } + }); + }); +}); diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/form/select/spinner.gif b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/form/select/spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..6ae1fce4076e7a92853c6a1fd9ba6e355b028d20 GIT binary patch literal 1659 zcmZvcdrVVT0Ef?g^~G(yv{&o{LJ_D%saG;cMN?n7CQw5~PpWI{%1npAiLs%YP-`61A_qd zoQ%=Bs1#!MwR^;s9ZLP_6xxa4gPmiSvS8+(k%NC!hXIOqj7Otx9aMQJj>&X6S2Vqk zAd%n9&JTp!?m0du0HbCn{%|*jL!8P~(_#!skvU7q;)XzvBw6wX0|BP_f>`HcxF_u$ z6{Ub=7}TFMKn`khOn&QUyoumknM9SQ-A6DdWkXrqpGG@4sHrE+1Dk|ue!NSWfitgO zvnk|zKb}-AkkZBk2}M&4$f{jhZN}ZB55!T4X~9+N0HRDIt18wmvX=AzhcMN}R1Eio zJ_HW9_&8`-hSkr26PBhOn6ET;s~OWxwf!5S68 zs+1vxDQ9uIEt}LrC@D^}>}M0ow)CX~e~rDjL9ALqt*hC@S>)QhDOI&rCi_tKl7aKm z2cD0iz+}MWAukvVkHbf0Si=mMb!H1k>(98 z3?okI>R8358Suty%QH`0Y&x~?%aTq`QN3j1(0j@!qHSMg2V(P}80A*M%WGfLuxd)f z%-&Hp3gRA3@0US!iG(H7ZnmInXd;U(*8yG&T+ouUibOpOB3ey+mQJ6B6pY|vdbJ(r zZNsczTrXubpyxH#tqY;Sj_EkZ8JEi6lqrJ$<0=_&OP z{!acZW;OY$kr04s#FY#g_-VW6DOEieMV+}5Xju4H7~MzFS{lsD($L|t5g<=x&er=98tQgK)i z%oSCW?2Sg9dEUIBw^ritr6BKHe}A*S&)As}{6nitZtZ&R?<}h5nYlNvWY6RRp=$!4 z523|9J3}?L+q({Htg}zb`Hfu9={&W#-)!lZW&salrZ)*k-E}H!H-4&?0XYe(MaM5? zgKEIUodUQ4uvs#{T;uActHrNzK)$bGDhA)H&_r@ICCrmtV^>CdVg%HZf1?i5ckdHO ziBmTl1j|L94r?fz)y#=2JR^;@AbHFSNllC%x}f#2kZ_p~SwUDp;sDeUO-9v_2UXDu z64w?BY*J7OFjlsN!xsAa1u^O*ktiDZY8Aj&#eWP(_e?k~5m&Vx71tAsQ}yd5oz=#h z)i-~K1tzubMz*`wyJIIxUZS1-3Nk3MjymtJh!fI1@RB1ovPMfGQZYtPQh`FyH~^m6 z@hVIY#%@tUzt}QlSvs@~FqkBk6JLZl#G*HH_*aKDUc(UFC%klg`7qUsIUQ2la+ixhUugwTFN~nSHNcHS}AsUS-FAb#*YWTP= jhIv_=K}@*E!?!EJZ-tmObkhel0LjrUfg|+33>N(Zv#9vS literal 0 HcmV?d00001 diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/form/taglib b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/form/taglib new file mode 100644 index 000000000..e69de29bb From 0e1e96a44ec78f28f559f10b89bcf572db863697 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Wed, 7 Jun 2017 11:31:23 +0100 Subject: [PATCH 09/40] [JENKINS-43507] Widen types of input parameter --- .../endpoints/BitbucketEndpointConfiguration.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java index 8845b3559..bb9cf3688 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java @@ -152,12 +152,12 @@ public synchronized List getEndpoints() { * * @param endpoints the list of endpoints. */ - public synchronized void setEndpoints(@CheckForNull List endpoints) { + public synchronized void setEndpoints(@CheckForNull List endpoints) { Jenkins.getActiveInstance().checkPermission(Jenkins.ADMINISTER); - endpoints = new ArrayList<>(Util.fixNull(endpoints)); + List eps = new ArrayList<>(Util.fixNull(endpoints)); // remove duplicates and empty urls Set serverUrls = new HashSet(); - for (ListIterator iterator = endpoints.listIterator(); iterator.hasNext(); ) { + for (ListIterator iterator = eps.listIterator(); iterator.hasNext(); ) { AbstractBitbucketEndpoint endpoint = iterator.next(); String serverUrl = endpoint.getServerUrl(); if (StringUtils.isBlank(serverUrl) || serverUrls.contains(serverUrl)) { @@ -169,10 +169,10 @@ public synchronized void setEndpoints(@CheckForNull List Date: Wed, 7 Jun 2017 16:10:56 +0100 Subject: [PATCH 10/40] [JENKINS-43507] Add source context and request --- .../bitbucket/BitbucketSCMSourceContext.java | 284 ++++++++++++++ .../bitbucket/BitbucketSCMSourceRequest.java | 367 ++++++++++++++++++ 2 files changed, 651 insertions(+) create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceContext.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceRequest.java diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceContext.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceContext.java new file mode 100644 index 000000000..6ca662462 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceContext.java @@ -0,0 +1,284 @@ +/* + * 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.TaskListener; +import java.util.EnumSet; +import java.util.Set; +import jenkins.scm.api.SCMHeadObserver; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.SCMSourceCriteria; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; +import jenkins.scm.api.trait.SCMSourceContext; + +/** + * The {@link SCMSourceContext} for bitbucket. + * + * @since 2.2.0 + */ +public class BitbucketSCMSourceContext extends SCMSourceContext { + /** + * {@code true} if the {@link BitbucketSCMSourceRequest} will need information about branches. + */ + private boolean wantBranches; + /** + * {@code true} if the {@link BitbucketSCMSourceRequest} will need information about tags. + */ + private boolean wantTags; + /** + * {@code true} if the {@link BitbucketSCMSourceRequest} will need information about origin pull requests. + */ + private boolean wantOriginPRs; + /** + * {@code true} if the {@link BitbucketSCMSourceRequest} will need information about fork pull requests. + */ + private boolean wantForkPRs; + /** + * Set of {@link ChangeRequestCheckoutStrategy} to create for each origin pull request. + */ + @NonNull + private Set originPRStrategies = EnumSet.noneOf(ChangeRequestCheckoutStrategy.class); + /** + * Set of {@link ChangeRequestCheckoutStrategy} to create for each fork pull request. + */ + @NonNull + private Set forkPRStrategies = EnumSet.noneOf(ChangeRequestCheckoutStrategy.class); + /** + * The {@link WebhookRegistration} to use in this context. + */ + @NonNull + private WebhookRegistration webhookRegistration = WebhookRegistration.SYSTEM; + /** + * {@code true} if notifications should be disabled in this context. + */ + private boolean notificationsDisabled; + + /** + * Constructor. + * + * @param criteria (optional) criteria. + * @param observer the {@link SCMHeadObserver}. + */ + public BitbucketSCMSourceContext(@CheckForNull SCMSourceCriteria criteria, + @NonNull SCMHeadObserver observer) { + super(criteria, observer); + } + + /** + * Returns {@code true} if the {@link BitbucketSCMSourceRequest} will need information about branches. + * + * @return {@code true} if the {@link BitbucketSCMSourceRequest} will need information about branches. + */ + public final boolean wantBranches() { + return wantBranches; + } + + /** + * Returns {@code true} if the {@link BitbucketSCMSourceRequest} will need information about tags. + * + * @return {@code true} if the {@link BitbucketSCMSourceRequest} will need information about tags. + */ + public final boolean wantTags() { + return wantTags; + } + + /** + * Returns {@code true} if the {@link BitbucketSCMSourceRequest} will need information about pull requests. + * + * @return {@code true} if the {@link BitbucketSCMSourceRequest} will need information about pull requests. + */ + public final boolean wantPRs() { + return wantOriginPRs || wantForkPRs; + } + + /** + * Returns {@code true} if the {@link BitbucketSCMSourceRequest} will need information about origin pull requests. + * + * @return {@code true} if the {@link BitbucketSCMSourceRequest} will need information about origin pull requests. + */ + public final boolean wantOriginPRs() { + return wantOriginPRs; + } + + /** + * Returns {@code true} if the {@link BitbucketSCMSourceRequest} will need information about fork pull requests. + * + * @return {@code true} if the {@link BitbucketSCMSourceRequest} will need information about fork pull requests. + */ + public final boolean wantForkPRs() { + return wantForkPRs; + } + + /** + * Returns the set of {@link ChangeRequestCheckoutStrategy} to create for each origin pull request. + * + * @return the set of {@link ChangeRequestCheckoutStrategy} to create for each origin pull request. + */ + @NonNull + public final Set originPRStrategies() { + return originPRStrategies; + } + + /** + * Returns the set of {@link ChangeRequestCheckoutStrategy} to create for each fork pull request. + * + * @return the set of {@link ChangeRequestCheckoutStrategy} to create for each fork pull request. + */ + @NonNull + public final Set forkPRStrategies() { + return forkPRStrategies; + } + + /** + * Returns the {@link WebhookRegistration} mode. + * + * @return the {@link WebhookRegistration} mode. + */ + @NonNull + public final WebhookRegistration webhookRegistration() { + return webhookRegistration; + } + + /** + * Returns {@code true} if notifications shoule be disabled. + * + * @return {@code true} if notifications shoule be disabled. + */ + public final boolean notificationsDisabled() { + return notificationsDisabled; + } + + /** + * Adds a requirement for branch details to any {@link BitbucketSCMSourceRequest} for this context. + * + * @param include {@code true} to add the requirement or {@code false} to leave the requirement as is (makes + * simpler with method chaining) + * @return {@code this} for method chaining. + */ + @NonNull + public final BitbucketSCMSourceContext wantBranches(boolean include) { + wantBranches = wantBranches || include; + return this; + } + + /** + * Adds a requirement for tag details to any {@link BitbucketSCMSourceRequest} for this context. + * + * @param include {@code true} to add the requirement or {@code false} to leave the requirement as is (makes + * simpler with method chaining) + * @return {@code this} for method chaining. + */ + @NonNull + public final BitbucketSCMSourceContext wantTags(boolean include) { + wantTags = wantTags || include; + return this; + } + + /** + * Adds a requirement for origin pull request details to any {@link BitbucketSCMSourceRequest} for this context. + * + * @param include {@code true} to add the requirement or {@code false} to leave the requirement as is (makes + * simpler with method chaining) + * @return {@code this} for method chaining. + */ + @NonNull + public final BitbucketSCMSourceContext wantOriginPRs(boolean include) { + wantOriginPRs = wantOriginPRs || include; + return this; + } + + /** + * Adds a requirement for fork pull request details to any {@link BitbucketSCMSourceRequest} for this context. + * + * @param include {@code true} to add the requirement or {@code false} to leave the requirement as is (makes + * simpler with method chaining) + * @return {@code this} for method chaining. + */ + @NonNull + public final BitbucketSCMSourceContext wantForkPRs(boolean include) { + wantForkPRs = wantForkPRs || include; + return this; + } + + /** + * Defines the {@link ChangeRequestCheckoutStrategy} instances to create for each origin pull request. + * + * @param strategies the strategies. + * @return {@code this} for method chaining. + */ + @NonNull + public final BitbucketSCMSourceContext withOriginPRStrategies( + @NonNull Set strategies) { + originPRStrategies.addAll(strategies); + return this; + } + + /** + * Defines the {@link ChangeRequestCheckoutStrategy} instances to create for each fork pull request. + * + * @param strategies the strategies. + * @return {@code this} for method chaining. + */ + @NonNull + public final BitbucketSCMSourceContext withForkPRStrategies( + @NonNull Set strategies) { + forkPRStrategies.addAll(strategies); + return this; + } + + /** + * Defines the {@link WebhookRegistration} mode to use in this context. + * + * @param mode the mode. + * @return {@code this} for method chaining. + */ + @NonNull + public final BitbucketSCMSourceContext webhookRegistration(WebhookRegistration mode) { + webhookRegistration = mode; + return this; + } + + /** + * Defines the notification mode to use in this context. + * + * @param disabled {@code true} to disable automatic notifications. + * @return {@code this} for method chaining. + */ + @NonNull + public final BitbucketSCMSourceContext withNotificationsDisabled(boolean disabled) { + this.notificationsDisabled = disabled; + return this; + } + + /** + * {@inheritDoc} + */ + @NonNull + @Override + public BitbucketSCMSourceRequest newRequest(@NonNull SCMSource scmSource, TaskListener taskListener) { + return new BitbucketSCMSourceRequest((BitbucketSCMSource) scmSource, this, taskListener); + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceRequest.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceRequest.java new file mode 100644 index 000000000..a5936b9cd --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceRequest.java @@ -0,0 +1,367 @@ +/* + * 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.BitbucketBranch; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Util; +import hudson.model.TaskListener; +import java.io.Closeable; +import java.io.IOException; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMHeadOrigin; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; +import jenkins.scm.api.mixin.TagSCMHead; +import jenkins.scm.api.trait.SCMSourceRequest; + +/** + * The {@link SCMSourceRequest} for bitbucket. + * + * @since 2.2.0 + */ +public class BitbucketSCMSourceRequest extends SCMSourceRequest { + /** + * {@code true} if branch details need to be fetched. + */ + private final boolean fetchBranches; + /** + * {@code true} if tag details need to be fetched. + */ + private final boolean fetchTags; + /** + * {@code true} if origin pull requests need to be fetched. + */ + private final boolean fetchOriginPRs; + /** + * {@code true} if fork pull requests need to be fetched. + */ + private final boolean fetchForkPRs; + /** + * The {@link ChangeRequestCheckoutStrategy} to create for each origin pull request. + */ + @NonNull + private final Set originPRStrategies; + /** + * The {@link ChangeRequestCheckoutStrategy} to create for each fork pull request. + */ + @NonNull + private final Set forkPRStrategies; + /** + * The set of pull request numbers that the request is scoped to or {@code null} if the request is not limited. + */ + @CheckForNull + private final Set requestedPullRequestNumbers; + /** + * The set of origin branch names that the request is scoped to or {@code null} if the request is not limited. + */ + @CheckForNull + private final Set requestedOriginBranchNames; + /** + * The set of tag names that the request is scoped to or {@code null} if the request is not limited. + */ + @CheckForNull + private final Set requestedTagNames; + /** + * The {@link BitbucketSCMSource#getRepoOwner()}. + */ + @NonNull + private final String repoOwner; + /** + * The {@link BitbucketSCMSource#getRepository()}. + */ + @NonNull + private final String repository; + /** + * The pull request details or {@code null} if not {@link #isFetchPRs()}. + */ + @CheckForNull + private Iterable pullRequests; + /** + * The branch details or {@code null} if not {@link #isFetchBranches()}. + */ + @CheckForNull + private Iterable branches; + // TODO private Iterable tags; + + /** + * Constructor. + * + * @param source the source. + * @param context the context. + * @param listener the listener. + */ + protected BitbucketSCMSourceRequest(@NonNull BitbucketSCMSource source, + @NonNull BitbucketSCMSourceContext context, + @CheckForNull TaskListener listener) { + super(source, context, listener); + fetchBranches = context.wantBranches(); + fetchTags = context.wantTags(); + fetchOriginPRs = context.wantOriginPRs(); + fetchForkPRs = context.wantForkPRs(); + originPRStrategies = fetchOriginPRs && !context.originPRStrategies().isEmpty() + ? Collections.unmodifiableSet(EnumSet.copyOf(context.originPRStrategies())) + : Collections.emptySet(); + forkPRStrategies = fetchForkPRs && !context.forkPRStrategies().isEmpty() + ? Collections.unmodifiableSet(EnumSet.copyOf(context.forkPRStrategies())) + : Collections.emptySet(); + Set includes = context.observer().getIncludes(); + if (includes != null) { + Set pullRequestNumbers = new HashSet<>(includes.size()); + Set branchNames = new HashSet<>(includes.size()); + Set tagNames = new HashSet<>(includes.size()); + for (SCMHead h : includes) { + if (h instanceof BranchSCMHead) { + branchNames.add(h.getName()); + } else if (h instanceof PullRequestSCMHead) { + pullRequestNumbers.add(((PullRequestSCMHead) h).getId()); + if (SCMHeadOrigin.DEFAULT.equals(h.getOrigin())) { + branchNames.add(((PullRequestSCMHead) h).getOriginName()); + } + if (((PullRequestSCMHead) h).getCheckoutStrategy() == ChangeRequestCheckoutStrategy.MERGE) { + branchNames.add(((PullRequestSCMHead) h).getTarget().getName()); + } + } else if (h instanceof TagSCMHead) { // TODO replace with concrete class when tag support added + tagNames.add(h.getName()); + } + } + this.requestedPullRequestNumbers = Collections.unmodifiableSet(pullRequestNumbers); + this.requestedOriginBranchNames = Collections.unmodifiableSet(branchNames); + this.requestedTagNames = Collections.unmodifiableSet(tagNames); + } else { + requestedPullRequestNumbers = null; + requestedOriginBranchNames = null; + requestedTagNames = null; + } + repoOwner = source.getRepoOwner(); + repository = source.getRepository(); + } + + /** + * Returns {@code true} if branch details need to be fetched. + * + * @return {@code true} if branch details need to be fetched. + */ + public final boolean isFetchBranches() { + return fetchBranches; + } + + /** + * Returns {@code true} if tag details need to be fetched. + * + * @return {@code true} if tag details need to be fetched. + */ + public final boolean isFetchTags() { + return fetchTags; + } + + /** + * Returns {@code true} if pull request details need to be fetched. + * + * @return {@code true} if pull request details need to be fetched. + */ + public final boolean isFetchPRs() { + return isFetchOriginPRs() || isFetchForkPRs(); + } + + /** + * Returns {@code true} if origin pull request details need to be fetched. + * + * @return {@code true} if origin pull request details need to be fetched. + */ + public final boolean isFetchOriginPRs() { + return fetchOriginPRs; + } + + /** + * Returns {@code true} if fork pull request details need to be fetched. + * + * @return {@code true} if fork pull request details need to be fetched. + */ + public final boolean isFetchForkPRs() { + return fetchForkPRs; + } + + /** + * Returns the {@link ChangeRequestCheckoutStrategy} to create for each origin pull request. + * + * @return the {@link ChangeRequestCheckoutStrategy} to create for each origin pull request. + */ + @NonNull + public final Set getOriginPRStrategies() { + return originPRStrategies; + } + + /** + * Returns the {@link ChangeRequestCheckoutStrategy} to create for each fork pull request. + * + * @return the {@link ChangeRequestCheckoutStrategy} to create for each fork pull request. + */ + @NonNull + public final Set getForkPRStrategies() { + return forkPRStrategies; + } + + /** + * Returns the {@link ChangeRequestCheckoutStrategy} to create for pull requests of the specified type. + * + * @param fork {@code true} to return strategies for the fork pull requests, {@code false} for origin pull requests. + * @return the {@link ChangeRequestCheckoutStrategy} to create for each pull request. + */ + @NonNull + public final Set getPRStrategies(boolean fork) { + if (fork) { + return fetchForkPRs ? getForkPRStrategies() : Collections.emptySet(); + } + return fetchOriginPRs ? getOriginPRStrategies() : Collections.emptySet(); + } + + /** + * Returns the {@link ChangeRequestCheckoutStrategy} to create for each pull request. + * + * @return a map of the {@link ChangeRequestCheckoutStrategy} to create for each pull request keyed by whether the + * strategy applies to forks or not ({@link Boolean#FALSE} is the key for origin pull requests) + */ + public final Map> getPRStrategies() { + Map> result = new HashMap<>(); + for (Boolean fork : new Boolean[]{Boolean.TRUE, Boolean.FALSE}) { + result.put(fork, getPRStrategies(fork)); + } + return result; + } + + /** + * Returns requested pull request numbers. + * + * @return the requested pull request numbers or {@code null} if the request was not scoped to a subset of pull + * requests. + */ + @CheckForNull + public final Set getRequestedPullRequestNumbers() { + return requestedPullRequestNumbers; + } + + /** + * Gets requested origin branch names. + * + * @return the requested origin branch names or {@code null} if the request was not scoped to a subset of branches. + */ + @CheckForNull + public final Set getRequestedOriginBranchNames() { + return requestedOriginBranchNames; + } + + /** + * Gets requested tag names. + * + * @return the requested tag names or {@code null} if the request was not scoped to a subset of tags. + */ + @CheckForNull + public final Set getRequestedTagNames() { + return requestedTagNames; + } + + /** + * Returns the {@link BitbucketSCMSource#getRepoOwner()} + * + * @return the {@link BitbucketSCMSource#getRepoOwner()} + */ + @NonNull + public final String getRepoOwner() { + return repoOwner; + } + + /** + * Returns the {@link BitbucketSCMSource#getRepository()}. + * + * @return the {@link BitbucketSCMSource#getRepository()}. + */ + @NonNull + public final String getRepository() { + return repository; + } + + /** + * Provides the requests with the pull request details. + * + * @param pullRequests the pull request details. + */ + public final void setPullRequests(@CheckForNull Iterable pullRequests) { + this.pullRequests = pullRequests; + } + + /** + * Returns the pull request details or an empty list if either the request did not specify to {@link #isFetchPRs()} + * or if the pull request details have not been provided by {@link #setPullRequests(Iterable)} yet. + * + * @return the pull request details (may be empty) + */ + @NonNull + public final Iterable getPullRequests() { + return Util.fixNull(pullRequests); + } + + /** + * Provides the requests with the branch details. + * + * @param branches the branch details. + */ + public final void setBranches(@CheckForNull Iterable branches) { + this.branches = branches; + } + + /** + * Returns the branch details or an empty list if either the request did not specify to {@link #isFetchBranches()} + * or if the branch details have not been provided by {@link #setBranches(Iterable)} yet. + * + * @return the branch details (may be empty) + */ + @NonNull + public final Iterable getBranches() { + return Util.fixNull(branches); + } + + // TODO Iterable getTags() and setTags(...) + + /** + * {@inheritDoc} + */ + @Override + public void close() throws IOException { + if (pullRequests instanceof Closeable) { + ((Closeable) pullRequests).close(); + } + if (branches instanceof Closeable) { + ((Closeable) branches).close(); + } + super.close(); + } +} From 933a75ad35f20613834c34a41729213758258cbe Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Wed, 7 Jun 2017 16:24:38 +0100 Subject: [PATCH 11/40] [JENKINS-43507] Add navigator context and request --- .../BitbucketSCMNavigatorContext.java | 47 +++++++++++++++++ .../BitbucketSCMNavigatorRequest.java | 51 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorContext.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorRequest.java diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorContext.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorContext.java new file mode 100644 index 000000000..cf6729e68 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorContext.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 edu.umd.cs.findbugs.annotations.NonNull; +import jenkins.scm.api.SCMNavigator; +import jenkins.scm.api.SCMSourceObserver; +import jenkins.scm.api.trait.SCMNavigatorContext; + +/** + * The {@link SCMNavigatorContext} for bitbucket. + * + * @since 2.2.0 + */ +public class BitbucketSCMNavigatorContext + extends SCMNavigatorContext { + /** + * {@inheritDoc} + */ + @NonNull + @Override + public BitbucketSCMNavigatorRequest newRequest(@NonNull SCMNavigator navigator, + @NonNull SCMSourceObserver observer) { + return new BitbucketSCMNavigatorRequest(navigator, this, observer); + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorRequest.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorRequest.java new file mode 100644 index 000000000..801afab0d --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorRequest.java @@ -0,0 +1,51 @@ +/* + * 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 jenkins.scm.api.SCMNavigator; +import jenkins.scm.api.SCMSourceObserver; +import jenkins.scm.api.trait.SCMNavigatorRequest; + +/** + * The {@link SCMNavigatorRequest} for bitbucket. + * + * @since 2.2.0 + */ +public class BitbucketSCMNavigatorRequest extends SCMNavigatorRequest { + /** + * Constructor. + * + * @param source the source. + * @param context the context. + * @param observer the observer. + */ + protected BitbucketSCMNavigatorRequest(@NonNull SCMNavigator source, + @NonNull + BitbucketSCMNavigatorContext context, + @NonNull + SCMSourceObserver observer) { + super(source, context, observer); + } +} From 8f636d6c96f0d51812680149781a00ce7a6dfc40 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Thu, 8 Jun 2017 11:56:31 +0100 Subject: [PATCH 12/40] [JENKINS-40475] Use a more resilient API call to determine the commit of a PR --- .../client/BitbucketCloudApiClient.java | 13 ++++++--- .../BitbucketPullRequestCommit.java | 16 +++++++++++ .../BitbucketPullRequestCommits.java | 28 +++++++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/pullrequest/BitbucketPullRequestCommit.java create mode 100644 src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/pullrequest/BitbucketPullRequestCommits.java diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java index 29d802368..597cac565 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java @@ -36,6 +36,8 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook; 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.BitbucketPullRequestCommit; +import com.cloudbees.jenkins.plugins.bitbucket.client.pullrequest.BitbucketPullRequestCommits; import com.cloudbees.jenkins.plugins.bitbucket.client.pullrequest.BitbucketPullRequestValue; import com.cloudbees.jenkins.plugins.bitbucket.client.pullrequest.BitbucketPullRequests; import com.cloudbees.jenkins.plugins.bitbucket.client.repository.BitbucketCloudRepository; @@ -341,12 +343,15 @@ public BitbucketCommit resolveCommit(@NonNull String hash) throws IOException, I @NonNull @Override public String resolveSourceFullHash(@NonNull BitbucketPullRequest pull) throws IOException, InterruptedException { - String url = V2_API_BASE_URL + pull.getSource().getRepository().getOwnerName() + "/" + - pull.getSource().getRepository().getRepositoryName() + "/commit/" + pull.getSource().getCommit() - .getHash(); + String url = V2_API_BASE_URL + owner + "/" + repositoryName + "/pullrequests/" + pull.getId() + + "/commits?fields=values.hash&pagelen=1"; String response = getRequest(url); try { - return parse(response, BitbucketCloudCommit.class).getHash(); + BitbucketPullRequestCommits commits = parse(response, BitbucketPullRequestCommits.class); + for (BitbucketPullRequestCommit commit : Util.fixNull(commits.getValues())) { + return commit.getHash(); + } + throw new BitbucketException("Could not determine commit for pull request " + pull.getId()); } catch (IOException e) { throw new IOException("I/O error when parsing response from URL: " + url, e); } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/pullrequest/BitbucketPullRequestCommit.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/pullrequest/BitbucketPullRequestCommit.java new file mode 100644 index 000000000..9fb495692 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/pullrequest/BitbucketPullRequestCommit.java @@ -0,0 +1,16 @@ +package com.cloudbees.jenkins.plugins.bitbucket.client.pullrequest; + +import org.codehaus.jackson.annotate.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class BitbucketPullRequestCommit { + private String hash; + + public String getHash() { + return hash; + } + + public void setHash(String hash) { + this.hash = hash; + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/pullrequest/BitbucketPullRequestCommits.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/pullrequest/BitbucketPullRequestCommits.java new file mode 100644 index 000000000..010bbc09b --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/pullrequest/BitbucketPullRequestCommits.java @@ -0,0 +1,28 @@ +package com.cloudbees.jenkins.plugins.bitbucket.client.pullrequest; + +import java.util.List; +import org.codehaus.jackson.annotate.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class BitbucketPullRequestCommits { + private String next; + + private List values; + + public String getNext() { + return next; + } + + public void setNext(String next) { + this.next = next; + } + + public List getValues() { + return values; + } + + public void setValues(List values) { + this.values = values; + } + +} From 95aac946ac4986ff5461bf3e879fd18d476e053f Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Thu, 8 Jun 2017 14:16:35 +0100 Subject: [PATCH 13/40] [JENKINS-43507] checkPathExists can take a branch OR a hash --- .../jenkins/plugins/bitbucket/api/BitbucketApi.java | 11 +++++------ .../bitbucket/client/BitbucketCloudApiClient.java | 5 +++-- .../server/client/BitbucketServerAPIClient.java | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java index 1af9033b7..eb30aab88 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java @@ -23,14 +23,12 @@ */ package com.cloudbees.jenkins.plugins.bitbucket.api; +import com.cloudbees.jenkins.plugins.bitbucket.client.repository.UserRoleInRepository; +import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; import java.util.List; -import com.cloudbees.jenkins.plugins.bitbucket.client.repository.UserRoleInRepository; - -import edu.umd.cs.findbugs.annotations.CheckForNull; - /** * Provides access to a specific repository. * One API object needs to be created for each repository you want to work with. @@ -111,13 +109,14 @@ String getRepositoryUri(@NonNull BitbucketRepositoryType type, /** * Checks if the given path exists in the repository at the specified branch. * - * @param branch the branch name + * @param branchOrHash the branch name or commit hash * @param path the path to check for * @return true if the path exists * @throws IOException if there was a network communications error. * @throws InterruptedException if interrupted while waiting on remote communications. */ - boolean checkPathExists(@NonNull String branch, @NonNull String path) throws IOException, InterruptedException; + boolean checkPathExists(@NonNull String branchOrHash, @NonNull String path) + throws IOException, InterruptedException; /** * Gets the default branch in the repository. diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java index 597cac565..6499c6d99 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java @@ -271,13 +271,14 @@ public void deletePullRequestApproval(String pullRequestId) throws IOException, * {@inheritDoc} */ @Override - public boolean checkPathExists(@NonNull String branch, @NonNull String path) throws IOException, InterruptedException { + public boolean checkPathExists(@NonNull String branchOrHash, @NonNull String path) + throws IOException, InterruptedException { StringBuilder url = new StringBuilder(V1_API_BASE_URL); url.append(owner); url.append('/'); url.append(repositoryName); url.append("/raw/"); - url.append(Util.rawEncode(branch)); + url.append(Util.rawEncode(branchOrHash)); for (String segment : StringUtils.split(path, "/")) { url.append('/'); url.append(Util.rawEncode(segment)); diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java index 681dbe5c3..1afe8ba1e 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java @@ -318,7 +318,7 @@ public void postBuildStatus(@NonNull BitbucketBuildStatus status) throws IOExcep * {@inheritDoc} */ @Override - public boolean checkPathExists(@NonNull String branch, @NonNull String path) throws IOException { + public boolean checkPathExists(@NonNull String branchOrHash, @NonNull String path) throws IOException { StringBuilder encodedPath = new StringBuilder(path.length() + 10); boolean first = true; for (String segment : StringUtils.split(path, "/")) { @@ -330,7 +330,7 @@ public boolean checkPathExists(@NonNull String branch, @NonNull String path) thr encodedPath.append(Util.rawEncode(segment)); } int status = getRequestStatus(String.format(API_BROWSE_PATH, getUserCentricOwner(), repositoryName, encodedPath, - URLEncoder.encode(branch, "UTF-8"))); + URLEncoder.encode(branchOrHash, "UTF-8"))); return HttpStatus.SC_OK == status; } From 50166af3f4511f86b994b897b5da068d4d721fea Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Fri, 9 Jun 2017 11:28:40 +0100 Subject: [PATCH 14/40] [JENKINS-43507] Add API to derive web links to endpoint configuration --- .../endpoints/AbstractBitbucketEndpoint.java | 10 ++++++++++ .../bitbucket/endpoints/BitbucketCloudEndpoint.java | 10 ++++++++++ .../bitbucket/endpoints/BitbucketServerEndpoint.java | 11 +++++++++++ .../AbstractBitbucketEndpointDescriptorTest.java | 8 ++++++++ .../endpoints/AbstractBitbucketEndpointTest.java | 7 +++++++ .../endpoints/BitbucketCloudEndpointTest.java | 6 ++++++ .../endpoints/BitbucketServerEndpointTest.java | 10 ++++++++++ 7 files changed, 62 insertions(+) diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint.java index 73dfcd1b7..1126cd587 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint.java @@ -80,6 +80,16 @@ public abstract class AbstractBitbucketEndpoint extends AbstractDescribableImpl< @NonNull public abstract String getServerUrl(); + /** + * The user facing URL of the specified repository. + * + * @param repoOwner the repository owner. + * @param repository the repository. + * @return the user facing URL of the specified repository. + */ + @NonNull + public abstract String getRepositoryUrl(@NonNull String repoOwner, @NonNull String repository); + /** * Returns {@code true} if and only if Jenkins is supposed to auto-manage hooks for this end-point. * diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpoint.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpoint.java index 8caf44805..360ee673f 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpoint.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpoint.java @@ -27,6 +27,7 @@ import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; +import hudson.Util; import javax.annotation.Nonnull; import org.kohsuke.stapler.DataBoundConstructor; @@ -71,6 +72,15 @@ public String getServerUrl() { return SERVER_URL; } + /** + * {@inheritDoc} + */ + @NonNull + @Override + public String getRepositoryUrl(@NonNull String repoOwner, @NonNull String repository) { + return SERVER_URL + "/" + Util.rawEncode(repoOwner) + "/" + Util.rawEncode(repository); + } + /** * Our descriptor. */ diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint.java index 292f8926a..8dff3dcc7 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint.java @@ -86,6 +86,17 @@ public String getServerUrl() { return serverUrl; } + /** + * {@inheritDoc} + */ + @NonNull + @Override + public String getRepositoryUrl(@NonNull String repoOwner, @NonNull String repository) { + return serverUrl + (repoOwner.startsWith("~") + ? "/users/" + Util.rawEncode(repoOwner.substring(1)) + : "/projects/" + Util.rawEncode(repoOwner)) + "/repos/" + Util.rawEncode(repository); + } + /** * Our descriptor. */ diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpointDescriptorTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpointDescriptorTest.java index b49d2a365..7eaf710ea 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpointDescriptorTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpointDescriptorTest.java @@ -31,6 +31,7 @@ import com.cloudbees.plugins.credentials.domains.HostnameSpecification; import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Util; import hudson.util.ListBoxModel; import java.util.Collections; import java.util.List; @@ -95,6 +96,13 @@ public String getServerUrl() { return "http://dummy.example.com"; } + + @NonNull + @Override + public String getRepositoryUrl(@NonNull String repoOwner, @NonNull String repository) { + return "http://dummy.example.com/" + Util.rawEncode(repoOwner) + "/" + Util.rawEncode(repository); + } + @TestExtension public static class DescriptorImpl extends AbstractBitbucketEndpointDescriptor { diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpointTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpointTest.java index 2a4d9edb9..5b63f4a4e 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpointTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpointTest.java @@ -29,6 +29,7 @@ import com.cloudbees.plugins.credentials.domains.Domain; import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Util; import java.util.Collections; import java.util.List; import org.junit.Before; @@ -114,6 +115,12 @@ public String getDisplayName() { public String getServerUrl() { return "http://dummy.example.com"; } + + @NonNull + @Override + public String getRepositoryUrl(@NonNull String repoOwner, @NonNull String repository) { + return "http://dummy.example.com/" + Util.rawEncode(repoOwner) + "/" + Util.rawEncode(repository); + } } } diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpointTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpointTest.java index 203d34e39..8073bfead 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpointTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpointTest.java @@ -37,4 +37,10 @@ public void smokes() { assertThat(new BitbucketCloudEndpoint(false, null).getServerUrl(), is(BitbucketCloudEndpoint.SERVER_URL)); } + @Test + public void getRepositoryUrl() { + assertThat(new BitbucketCloudEndpoint(false, null).getRepositoryUrl("tester", "test-repo"), + is("https://bitbucket.org/tester/test-repo")); + } + } diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpointTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpointTest.java index 2a6375e91..64fa51c1a 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpointTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpointTest.java @@ -40,6 +40,16 @@ public void smokes() { "http://dummy.example.com")); } + @Test + public void getRepositoryUrl() { + assertThat(new BitbucketServerEndpoint("Dummy", "http://dummy.example.com", false, null) + .getRepositoryUrl("TST", "test-repo"), + is("http://dummy.example.com/projects/TST/repos/test-repo")); + assertThat(new BitbucketServerEndpoint("Dummy", "http://dummy.example.com", false, null) + .getRepositoryUrl("~tester", "test-repo"), + is("http://dummy.example.com/users/tester/repos/test-repo")); + } + @Test public void given__badUrl__when__check__then__fail() { BitbucketServerEndpoint.DescriptorImpl descriptor = new BitbucketServerEndpoint.DescriptorImpl(); From a4c6bf39b83168ff62fc622bd4084ef90cf810c0 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Mon, 12 Jun 2017 16:04:15 +0100 Subject: [PATCH 15/40] [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 + From 71b6caea25462045afc7669851dc0b9571628d09 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Mon, 12 Jun 2017 16:24:37 +0100 Subject: [PATCH 16/40] [JENKINS-43507] Curses upon you IntelliJ with your "Reformat code" checkbox --- .../bitbucket/BitbucketSCMSourceTest/basic_cloud_git.xml | 4 +--- .../bitbucket/BitbucketSCMSourceTest/basic_cloud_hg.xml | 3 +-- .../plugins/bitbucket/BitbucketSCMSourceTest/basic_server.xml | 3 +-- .../BitbucketSCMSourceTest/custom_checkout_credentials.xml | 4 +--- .../bitbucket/BitbucketSCMSourceTest/exclude_branches.xml | 4 +--- .../bitbucket/BitbucketSCMSourceTest/limit_branches.xml | 4 +--- .../bitbucket/BitbucketSCMSourceTest/register_hooks.xml | 4 +--- .../bitbucket/BitbucketSCMSourceTest/use_agent_checkout.xml | 4 +--- 8 files changed, 8 insertions(+), 22 deletions(-) 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 index 87edb8039..b0bb2fc06 100644 --- 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 @@ -1,7 +1,5 @@ - - com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure - + com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure bitbucket-cloud SAME cloudbeers 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 index 12c7fc9f4..e434e737a 100644 --- 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 @@ -1,6 +1,5 @@ - com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::shiny-telegram - + com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::shiny-telegram bitbucket-cloud SAME cloudbeers 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 index e2c621625..55ede2f11 100644 --- 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 @@ -1,6 +1,5 @@ - com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.test::DUB::stunning-adventure - + com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.test::DUB::stunning-adventure bb-beescloud SAME DUB 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 index 1ad61472b..365c5dd46 100644 --- 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 @@ -1,7 +1,5 @@ - - com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure - + com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure bitbucket-cloud other-credentials cloudbeers 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 index 72403d596..23a54ef3c 100644 --- 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 @@ -1,7 +1,5 @@ - - com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure - + com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure bitbucket-cloud SAME cloudbeers 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 index 3e90f4566..380fb5a76 100644 --- 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 @@ -1,7 +1,5 @@ - - com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure - + com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure bitbucket-cloud SAME cloudbeers 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 index b2a22ea4e..a4ecaa931 100644 --- 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 @@ -1,7 +1,5 @@ - - com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure - + com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure bitbucket-cloud SAME cloudbeers 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 index b1d93aa0a..704861fb4 100644 --- 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 @@ -1,7 +1,5 @@ - - com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure - + com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::cloudbeers::stunning-adventure bitbucket-cloud ANONYMOUS cloudbeers From 0d083dec87b7a660b36c9e191a9c441ed2ce5c73 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Mon, 12 Jun 2017 21:41:33 +0100 Subject: [PATCH 17/40] [JENKINS-43507] Pick up latest timestamp snapshots --- pom.xml | 269 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 136 insertions(+), 133 deletions(-) diff --git a/pom.xml b/pom.xml index 3629d2e3d..bfbb083f0 100644 --- a/pom.xml +++ b/pom.xml @@ -22,42 +22,45 @@ ~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ~ THE SOFTWARE. --> - - 4.0.0 + + 4.0.0 - - org.jenkins-ci.plugins - plugin - 2.22 - - + + org.jenkins-ci.plugins + plugin + 2.22 + + - cloudbees-bitbucket-branch-source + cloudbees-bitbucket-branch-source 2.2.0-SNAPSHOT - hpi + hpi - Bitbucket Branch Source Plugin - https://wiki.jenkins-ci.org/display/JENKINS/Bitbucket+Branch+Source+Plugin - Discover and build Bitbucket Cloud and Bitbucket Server pull requests and branches and send status notifications with the build result. - - - MIT License - http://opensource.org/licenses/MIT - - + Bitbucket Branch Source Plugin + https://wiki.jenkins-ci.org/display/JENKINS/Bitbucket+Branch+Source+Plugin + Discover and build Bitbucket Cloud and Bitbucket Server pull requests and branches and send status + notifications with the build result. + + + + MIT License + http://opensource.org/licenses/MIT + + - - 1.642.3 - 2.2.0-SNAPSHOT - 3.4.0-SNAPSHOT - + + 1.642.3 + 2.2.0-20170612.200107-11 + 3.4.0-20170612.203958-2 + - - scm:git:git://github.com/jenkinsci/bitbucket-branch-source-plugin.git - scm:git:git@github.com:jenkinsci/bitbucket-branch-source-plugin.git - https://github.com/jenkinsci/bitbucket-branch-source-plugin - HEAD - + + scm:git:git://github.com/jenkinsci/bitbucket-branch-source-plugin.git + scm:git:git@github.com:jenkinsci/bitbucket-branch-source-plugin.git + https://github.com/jenkinsci/bitbucket-branch-source-plugin + HEAD + @@ -72,111 +75,111 @@ org.jenkins-ci.plugins scm-api - - - org.jenkins-ci.plugins - git - ${git.version} - - - org.apache.httpcomponents - httpclient - - - - - org.jenkins-ci.plugins - mercurial - 2.0-SNAPSHOT - - - org.codehaus.jackson - jackson-jaxrs - 1.9.13 - - - org.jenkins-ci.plugins - display-url-api - 0.2 - - - org.jenkins-ci.plugins - branch-api - 2.0.10-SNAPSHOT - test - - - org.jenkins-ci.plugins - scm-api - ${scm-api.version} - tests - test - - - org.mockito - mockito-core - 1.10.19 - test - - - org.hamcrest - hamcrest-core - 1.3 - test - - - org.jenkins-ci.plugins - git - ${git.version} - tests - test - - - org.jenkins-ci.plugins.workflow - workflow-multibranch - 2.11 - test - - - org.jenkins-ci.plugins.workflow - workflow-aggregator - 1.15 - tests - test - +
+ + org.jenkins-ci.plugins + git + ${git.version} + + + org.apache.httpcomponents + httpclient + + + + + org.jenkins-ci.plugins + mercurial + 2.0-20170612.200544-1 + + + org.codehaus.jackson + jackson-jaxrs + 1.9.13 + + + org.jenkins-ci.plugins + display-url-api + 0.2 + + + org.jenkins-ci.plugins + branch-api + 2.0.11-20170612.201016-1 + test + + + org.jenkins-ci.plugins + scm-api + ${scm-api.version} + tests + test + + + org.mockito + mockito-core + 1.10.19 + test + + + org.hamcrest + hamcrest-core + 1.3 + test + + + org.jenkins-ci.plugins + git + ${git.version} + tests + test + + + org.jenkins-ci.plugins.workflow + workflow-multibranch + 2.11 + test + + + org.jenkins-ci.plugins.workflow + workflow-aggregator + 1.15 + tests + test + -
+ - - - repo.jenkins-ci.org - http://repo.jenkins-ci.org/public/ - - - - - repo.jenkins-ci.org - http://repo.jenkins-ci.org/public/ - - + + + repo.jenkins-ci.org + http://repo.jenkins-ci.org/public/ + + + + + repo.jenkins-ci.org + http://repo.jenkins-ci.org/public/ + + - - - - org.jenkins-ci.tools - maven-hpi-plugin - - - - generate-taglib-interface - - - - - 2.0.0 - - - - + + + + org.jenkins-ci.tools + maven-hpi-plugin + + + + generate-taglib-interface + + + + + 2.0.0 + + + + From e0e18b24bfe1c115465a9edd46d255f2718ade7e Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Tue, 13 Jun 2017 10:40:24 +0100 Subject: [PATCH 18/40] [JENKINS-43507] Some code simplification --- .../ForkPullRequestDiscoveryTrait.java | 30 ++++++++++++++----- .../OriginPullRequestDiscoveryTrait.java | 30 ++++++++++++++----- .../plugins/bitbucket/SSHCheckoutTrait.java | 3 +- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait.java index 097ffd491..cff0ada42 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait.java @@ -30,6 +30,7 @@ import java.util.Collections; import java.util.EnumSet; import java.util.List; +import java.util.Set; import jenkins.scm.api.SCMHeadCategory; import jenkins.scm.api.SCMHeadOrigin; import jenkins.scm.api.SCMRevision; @@ -86,7 +87,7 @@ public ForkPullRequestDiscoveryTrait(int strategyId, * @param strategies the {@link ChangeRequestCheckoutStrategy} instances. * @param trust the authority. */ - public ForkPullRequestDiscoveryTrait(@NonNull EnumSet strategies, + public ForkPullRequestDiscoveryTrait(@NonNull Set strategies, @NonNull SCMHeadAuthority trust) { this((strategies.contains(ChangeRequestCheckoutStrategy.MERGE) ? 1 : 0) @@ -102,6 +103,26 @@ public int getStrategyId() { return strategyId; } + + /** + * Returns the strategies. + * + * @return the strategies. + */ + @NonNull + public Set getStrategies() { + switch (strategyId) { + case 1: + return EnumSet.of(ChangeRequestCheckoutStrategy.MERGE); + case 2: + return EnumSet.of(ChangeRequestCheckoutStrategy.HEAD); + case 3: + return EnumSet.of(ChangeRequestCheckoutStrategy.HEAD, ChangeRequestCheckoutStrategy.MERGE); + default: + return EnumSet.noneOf(ChangeRequestCheckoutStrategy.class); + } + } + /** * Gets the authority. * @@ -121,12 +142,7 @@ 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)); - } + ctx.withForkPRStrategies(getStrategies()); } /** diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait.java index ae1a8b4bd..9fdebca85 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait.java @@ -26,8 +26,8 @@ import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.util.ListBoxModel; -import java.util.Collections; import java.util.EnumSet; +import java.util.Set; import jenkins.scm.api.SCMHeadCategory; import jenkins.scm.api.SCMHeadOrigin; import jenkins.scm.api.SCMRevision; @@ -72,7 +72,7 @@ public OriginPullRequestDiscoveryTrait(int strategyId) { * * @param strategies the {@link ChangeRequestCheckoutStrategy} instances. */ - public OriginPullRequestDiscoveryTrait(EnumSet strategies) { + public OriginPullRequestDiscoveryTrait(Set strategies) { this((strategies.contains(ChangeRequestCheckoutStrategy.MERGE) ? 1 : 0) + (strategies.contains(ChangeRequestCheckoutStrategy.HEAD) ? 2 : 0)); } @@ -86,6 +86,25 @@ public int getStrategyId() { return strategyId; } + /** + * Returns the strategies. + * + * @return the strategies. + */ + @NonNull + public Set getStrategies() { + switch (strategyId) { + case 1: + return EnumSet.of(ChangeRequestCheckoutStrategy.MERGE); + case 2: + return EnumSet.of(ChangeRequestCheckoutStrategy.HEAD); + case 3: + return EnumSet.of(ChangeRequestCheckoutStrategy.HEAD, ChangeRequestCheckoutStrategy.MERGE); + default: + return EnumSet.noneOf(ChangeRequestCheckoutStrategy.class); + } + } + /** * {@inheritDoc} */ @@ -94,12 +113,7 @@ 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)); - } + ctx.withOriginPRStrategies(getStrategies()); } /** diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait.java index 171d75e81..2853bbe70 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait.java @@ -72,7 +72,8 @@ public class SSHCheckoutTrait extends SCMSourceTrait { * Constructor. * * @param credentialsId the {@link SSHUserPrivateKey#getId()} of the credentials to use or - * {@link BitbucketSCMSource.DescriptorImpl#ANONYMOUS} to + * {@link BitbucketSCMSource.DescriptorImpl#ANONYMOUS} to defer to the agent configured + * credentials (typically anonymous but not always) */ @DataBoundConstructor public SSHCheckoutTrait(@CheckForNull String credentialsId) { From b0c72b8568d5e09f2928e4f051c87f831997571c Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Thu, 15 Jun 2017 11:25:08 +0100 Subject: [PATCH 19/40] [JENKINS-43507] Address @amuniz code review comments --- .../bitbucket/BitbucketHgSCMBuilder.java | 8 ++++++++ .../bitbucket/BranchDiscoveryTrait.java | 6 ++++++ .../PublicRepoPullRequestFilterTrait.java | 20 +++++++++---------- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketHgSCMBuilder.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketHgSCMBuilder.java index 4047fc20d..22d6b8437 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketHgSCMBuilder.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketHgSCMBuilder.java @@ -49,6 +49,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMRevision; import jenkins.scm.api.SCMSource; @@ -61,6 +63,10 @@ * @since 2.2.0 */ public class BitbucketHgSCMBuilder extends MercurialSCMBuilder { + /** + * Our logger. + */ + private static final Logger LOGGER = Logger.getLogger(BitbucketHgSCMBuilder.class.getName()); /** * The {@link BitbucketSCMSource} who's {@link BitbucketSCMSource#getOwner()} can be used as the context for * resolving credentials. @@ -212,6 +218,8 @@ public BitbucketHgSCMBuilder withBitbucketSource() { if (h instanceof PullRequestSCMHead) { PullRequestSCMHead head = (PullRequestSCMHead) h; if (head.getCheckoutStrategy() == ChangeRequestCheckoutStrategy.MERGE) { + LOGGER.log(Level.WARNING, "Building MERGE commits of PRs of Mercurial based repositories on " + + "BitBucket Cloud is not currently supported, falling back to HEAD commit"); // TODO decorate with something that handles merge commits // FIXME file a Jenkins JIRA } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait.java index 980798fd1..d0d71ed03 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait.java @@ -52,6 +52,12 @@ public class BranchDiscoveryTrait extends SCMSourceTrait { /** * The strategy encoded as a bit-field. + *
+ *
Bit 0
+ *
Build branches that are not filed as a PR
+ *
Bit 1
+ *
Build branches that are filed as a PR
+ *
*/ private int strategyId; diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTrait.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTrait.java index 1ffc94203..54d789029 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTrait.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTrait.java @@ -1,13 +1,3 @@ -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 * @@ -32,6 +22,16 @@ * THE SOFTWARE. */ +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; + /** * A {@link SCMSourceTrait} that supresses all pull requests if the repository is public. * From 933aae0dc4e87b513c44ddf520098bc93fccba31 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Thu, 15 Jun 2017 21:41:57 +0100 Subject: [PATCH 20/40] [JENKINS-43507] Pick up changes for JENKINS-44891 --- pom.xml | 8 +- .../plugins/bitbucket/BitbucketSCMSource.java | 21 +- .../bitbucket/BitbucketSCMSourceBuilder.java | 3 +- .../bitbucket/BitbucketGitSCMBuilderTest.java | 214 +++++++++++------- .../bitbucket/BitbucketHgSCMBuilderTest.java | 2 +- .../bitbucket/BitbucketSCMSourceTest.java | 62 ++--- .../BranchScanningIntegrationTest.java | 23 +- .../plugins/bitbucket/BranchScanningTest.java | 13 +- .../bitbucket/SSHCheckoutTraitTest.java | 8 +- .../bitbucket/WebhooksAutoregisterTest.java | 4 +- .../integration/ScanningFailuresTest.java | 50 +++- 11 files changed, 272 insertions(+), 136 deletions(-) diff --git a/pom.xml b/pom.xml index bfbb083f0..f110a2372 100644 --- a/pom.xml +++ b/pom.xml @@ -51,8 +51,8 @@ 1.642.3 - 2.2.0-20170612.200107-11 - 3.4.0-20170612.203958-2 + 2.2.0-SNAPSHOT + 3.4.0-SNAPSHOT @@ -90,7 +90,7 @@ org.jenkins-ci.plugins mercurial - 2.0-20170612.200544-1 + 2.0-SNAPSHOT org.codehaus.jackson @@ -105,7 +105,7 @@ org.jenkins-ci.plugins branch-api - 2.0.11-20170612.201016-1 + 2.0.11-SNAPSHOT test 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 d05d0ddb8..20085f001 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java @@ -231,18 +231,31 @@ public class BitbucketSCMSource extends SCMSource { /** * Constructor. * - * @param id the id. * @param repoOwner the repository owner. * @param repository the repository name. + * @since 2.2.0 */ @DataBoundConstructor - public BitbucketSCMSource(@CheckForNull String id, @NonNull String repoOwner, @NonNull String repository) { - super(id); + public BitbucketSCMSource(@NonNull String repoOwner, @NonNull String repository) { this.serverUrl = BitbucketCloudEndpoint.SERVER_URL; this.repoOwner = repoOwner; this.repository = repository; this.traits = new ArrayList<>(); - this.traits.add(new BranchDiscoveryTrait(true, true)); + } + + /** + * Legacy Constructor. + * + * @param id the id. + * @param repoOwner the repository owner. + * @param repository the repository name. + * @deprecated use {@link #BitbucketSCMSource(String, String)} and {@link #setId(String)} + */ + @Deprecated + public BitbucketSCMSource(@CheckForNull String id, @NonNull String repoOwner, @NonNull String repository) { + this(repoOwner, repository); + setId(id); + 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())); diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceBuilder.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceBuilder.java index 26c511e7f..a0a9a7c58 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceBuilder.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceBuilder.java @@ -116,7 +116,8 @@ public final String repoOwner() { @NonNull @Override public BitbucketSCMSource build() { - BitbucketSCMSource result = new BitbucketSCMSource(id(), repoOwner(), projectName()); + BitbucketSCMSource result = new BitbucketSCMSource(repoOwner(), projectName()); + result.setId(id()); result.setServerUrl(serverUrl()); result.setCredentialsId(credentialsId()); result.setTraits(traits()); diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilderTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilderTest.java index 42e691de5..c6c3cf601 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilderTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketGitSCMBuilderTest.java @@ -24,6 +24,7 @@ import java.util.logging.Logger; import jenkins.branch.BranchSource; import jenkins.plugins.git.AbstractGitSCMSource; +import jenkins.plugins.git.GitSCMSourceDefaults; import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMHeadOrigin; import jenkins.scm.api.SCMRevision; @@ -39,6 +40,7 @@ import org.mockito.Mockito; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; @@ -55,7 +57,7 @@ public class BitbucketGitSCMBuilderTest { @Before public void setUp() throws IOException { owner = j.createProject(WorkflowMultiBranchProject.class); - source = new BitbucketSCMSource("test", "tester", "test-repo"); + source = new BitbucketSCMSource( "tester", "test-repo"); owner.setSourcesList(Collections.singletonList(new BranchSource(source))); source.setOwner(owner); SystemCredentialsProvider.getInstance().setDomainCredentialsMap(Collections.singletonMap(Domain.global(), @@ -115,10 +117,11 @@ public void given__cloud_branch_rev_anon__when__build__then__scmBuilt() throws E 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(actual.getExtensions(), containsInAnyOrder( + instanceOf(BuildChooserSetting.class), + instanceOf(GitSCMSourceDefaults.class)) + ); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); AbstractGitSCMSource.SpecificRevisionBuildChooser revChooser = (AbstractGitSCMSource.SpecificRevisionBuildChooser) chooser.getBuildChooser(); @@ -172,10 +175,11 @@ public void given__cloud_branch_rev_userpass__when__build__then__scmBuilt() thro 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(actual.getExtensions(), containsInAnyOrder( + instanceOf(BuildChooserSetting.class), + instanceOf(GitSCMSourceDefaults.class)) + ); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); AbstractGitSCMSource.SpecificRevisionBuildChooser revChooser = (AbstractGitSCMSource.SpecificRevisionBuildChooser) chooser.getBuildChooser(); @@ -229,10 +233,11 @@ public void given__cloud_branch_rev_userkey__when__build__then__scmBuilt() throw 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(actual.getExtensions(), containsInAnyOrder( + instanceOf(BuildChooserSetting.class), + instanceOf(GitSCMSourceDefaults.class)) + ); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); AbstractGitSCMSource.SpecificRevisionBuildChooser revChooser = (AbstractGitSCMSource.SpecificRevisionBuildChooser) chooser.getBuildChooser(); @@ -284,7 +289,7 @@ public void given__cloud_branch_norev_anon__when__build__then__scmBuilt() throws 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)); + assertThat(actual.getExtensions(), contains(instanceOf(GitSCMSourceDefaults.class))); } @Test @@ -328,7 +333,7 @@ public void given__cloud_branch_norev_userpass__when__build__then__scmBuilt() th 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)); + assertThat(actual.getExtensions(), contains(instanceOf(GitSCMSourceDefaults.class))); } @Test @@ -372,7 +377,7 @@ public void given__cloud_branch_norev_userkey__when__build__then__scmBuilt() thr 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)); + assertThat(actual.getExtensions(), contains(instanceOf(GitSCMSourceDefaults.class))); } @Test @@ -419,10 +424,11 @@ public void given__server_branch_rev_anon__when__build__then__scmBuilt() throws 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(actual.getExtensions(), containsInAnyOrder( + instanceOf(GitSCMSourceDefaults.class), + instanceOf(BuildChooserSetting.class) + )); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); AbstractGitSCMSource.SpecificRevisionBuildChooser revChooser = (AbstractGitSCMSource.SpecificRevisionBuildChooser) chooser.getBuildChooser(); @@ -478,10 +484,11 @@ public void given__server_branch_rev_userpass__when__build__then__scmBuilt() thr 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(actual.getExtensions(), containsInAnyOrder( + instanceOf(GitSCMSourceDefaults.class), + instanceOf(BuildChooserSetting.class) + )); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); AbstractGitSCMSource.SpecificRevisionBuildChooser revChooser = (AbstractGitSCMSource.SpecificRevisionBuildChooser) chooser.getBuildChooser(); @@ -536,10 +543,11 @@ public void given__server_branch_rev_userkey__when__build__then__scmBuilt() thro 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(actual.getExtensions(), containsInAnyOrder( + instanceOf(GitSCMSourceDefaults.class), + instanceOf(BuildChooserSetting.class) + )); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); AbstractGitSCMSource.SpecificRevisionBuildChooser revChooser = (AbstractGitSCMSource.SpecificRevisionBuildChooser) chooser.getBuildChooser(); @@ -592,7 +600,7 @@ public void given__server_branch_norev_anon__when__build__then__scmBuilt() throw 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)); + assertThat(actual.getExtensions(), contains(instanceOf(GitSCMSourceDefaults.class))); } @Test @@ -637,7 +645,7 @@ public void given__server_branch_norev_userpass__when__build__then__scmBuilt() t 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)); + assertThat(actual.getExtensions(), contains(instanceOf(GitSCMSourceDefaults.class))); } @Test @@ -682,7 +690,7 @@ public void given__server_branch_norev_userkey__when__build__then__scmBuilt() th 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)); + assertThat(actual.getExtensions(), contains(instanceOf(GitSCMSourceDefaults.class))); } @@ -733,10 +741,11 @@ public void given__cloud_pullHead_rev_anon__when__build__then__scmBuilt() throws 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(actual.getExtensions(), containsInAnyOrder( + instanceOf(BuildChooserSetting.class), + instanceOf(GitSCMSourceDefaults.class)) + ); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); AbstractGitSCMSource.SpecificRevisionBuildChooser revChooser = (AbstractGitSCMSource.SpecificRevisionBuildChooser) chooser.getBuildChooser(); @@ -794,10 +803,11 @@ public void given__cloud_pullHead_rev_userpass__when__build__then__scmBuilt() th 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(actual.getExtensions(), containsInAnyOrder( + instanceOf(BuildChooserSetting.class), + instanceOf(GitSCMSourceDefaults.class)) + ); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); AbstractGitSCMSource.SpecificRevisionBuildChooser revChooser = (AbstractGitSCMSource.SpecificRevisionBuildChooser) chooser.getBuildChooser(); @@ -855,10 +865,11 @@ public void given__cloud_pullHead_rev_userkey__when__build__then__scmBuilt() thr 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(actual.getExtensions(), containsInAnyOrder( + instanceOf(BuildChooserSetting.class), + instanceOf(GitSCMSourceDefaults.class)) + ); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); AbstractGitSCMSource.SpecificRevisionBuildChooser revChooser = (AbstractGitSCMSource.SpecificRevisionBuildChooser) chooser.getBuildChooser(); @@ -912,7 +923,7 @@ public void given__cloud_pullHead_norev_anon__when__build__then__scmBuilt() thro 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)); + assertThat(actual.getExtensions(), contains(instanceOf(GitSCMSourceDefaults.class))); } @Test @@ -958,7 +969,7 @@ public void given__cloud_pullHead_norev_userpass__when__build__then__scmBuilt() 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)); + assertThat(actual.getExtensions(), contains(instanceOf(GitSCMSourceDefaults.class))); } @Test @@ -1004,7 +1015,7 @@ public void given__cloud_pullHead_norev_userkey__when__build__then__scmBuilt() t 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)); + assertThat(actual.getExtensions(), contains(instanceOf(GitSCMSourceDefaults.class))); } @Test @@ -1055,10 +1066,11 @@ public void given__server_pullHead_rev_anon__when__build__then__scmBuilt() throw 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(actual.getExtensions(), containsInAnyOrder( + instanceOf(BuildChooserSetting.class), + instanceOf(GitSCMSourceDefaults.class)) + ); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); AbstractGitSCMSource.SpecificRevisionBuildChooser revChooser = (AbstractGitSCMSource.SpecificRevisionBuildChooser) chooser.getBuildChooser(); @@ -1118,10 +1130,11 @@ public void given__server_pullHead_rev_userpass__when__build__then__scmBuilt() t 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(actual.getExtensions(), containsInAnyOrder( + instanceOf(BuildChooserSetting.class), + instanceOf(GitSCMSourceDefaults.class)) + ); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); AbstractGitSCMSource.SpecificRevisionBuildChooser revChooser = (AbstractGitSCMSource.SpecificRevisionBuildChooser) chooser.getBuildChooser(); @@ -1180,10 +1193,11 @@ public void given__server_pullHead_rev_userkey__when__build__then__scmBuilt() th 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(actual.getExtensions(), containsInAnyOrder( + instanceOf(BuildChooserSetting.class), + instanceOf(GitSCMSourceDefaults.class)) + ); + BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); AbstractGitSCMSource.SpecificRevisionBuildChooser revChooser = (AbstractGitSCMSource.SpecificRevisionBuildChooser) chooser.getBuildChooser(); @@ -1238,7 +1252,7 @@ public void given__server_pullHead_norev_anon__when__build__then__scmBuilt() thr 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)); + assertThat(actual.getExtensions(), contains(instanceOf(GitSCMSourceDefaults.class))); } @Test @@ -1285,7 +1299,7 @@ public void given__server_pullHead_norev_userpass__when__build__then__scmBuilt() 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)); + assertThat(actual.getExtensions(), contains(instanceOf(GitSCMSourceDefaults.class))); } @Test @@ -1332,7 +1346,7 @@ public void given__server_pullHead_norev_userkey__when__build__then__scmBuilt() 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)); + assertThat(actual.getExtensions(), contains(instanceOf(GitSCMSourceDefaults.class))); } @@ -1397,7 +1411,11 @@ public void given__cloud_pullMerge_rev_anon__when__build__then__scmBuilt() throw 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)); + assertThat(actual.getExtensions(), containsInAnyOrder( + instanceOf(GitSCMSourceDefaults.class), + instanceOf(BuildChooserSetting.class), + instanceOf(MergeWithGitSCMExtension.class) + )); BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); assertThat(chooser, notNullValue()); assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); @@ -1475,7 +1493,11 @@ public void given__cloud_pullMerge_rev_userpass__when__build__then__scmBuilt() t 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)); + assertThat(actual.getExtensions(), containsInAnyOrder( + instanceOf(GitSCMSourceDefaults.class), + instanceOf(BuildChooserSetting.class), + instanceOf(MergeWithGitSCMExtension.class) + )); BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); assertThat(chooser, notNullValue()); assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); @@ -1553,7 +1575,11 @@ public void given__cloud_pullMerge_rev_userkey__when__build__then__scmBuilt() th 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)); + assertThat(actual.getExtensions(), containsInAnyOrder( + instanceOf(GitSCMSourceDefaults.class), + instanceOf(BuildChooserSetting.class), + instanceOf(MergeWithGitSCMExtension.class) + )); BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); assertThat(chooser, notNullValue()); assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); @@ -1627,10 +1653,11 @@ public void given__cloud_pullMerge_norev_anon__when__build__then__scmBuilt() thr 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(actual.getExtensions(), containsInAnyOrder( + instanceOf(GitSCMSourceDefaults.class), + instanceOf(MergeWithGitSCMExtension.class) + )); + MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); assertThat(merge.getBaseHash(), is(nullValue())); } @@ -1692,10 +1719,11 @@ public void given__cloud_pullMerge_norev_userpass__when__build__then__scmBuilt() 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(actual.getExtensions(), containsInAnyOrder( + instanceOf(GitSCMSourceDefaults.class), + instanceOf(MergeWithGitSCMExtension.class) + )); + MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); assertThat(merge.getBaseHash(), is(nullValue())); } @@ -1757,10 +1785,11 @@ public void given__cloud_pullMerge_norev_userkey__when__build__then__scmBuilt() 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(actual.getExtensions(), containsInAnyOrder( + instanceOf(GitSCMSourceDefaults.class), + instanceOf(MergeWithGitSCMExtension.class) + )); + MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); assertThat(merge.getBaseHash(), is(nullValue())); } @@ -1827,7 +1856,11 @@ public void given__server_pullMerge_rev_anon__when__build__then__scmBuilt() thro 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)); + assertThat(actual.getExtensions(), containsInAnyOrder( + instanceOf(MergeWithGitSCMExtension.class), + instanceOf(BuildChooserSetting.class), + instanceOf(GitSCMSourceDefaults.class)) + ); BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); assertThat(chooser, notNullValue()); assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); @@ -1907,7 +1940,11 @@ public void given__server_pullMerge_rev_userpass__when__build__then__scmBuilt() 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)); + assertThat(actual.getExtensions(), containsInAnyOrder( + instanceOf(MergeWithGitSCMExtension.class), + instanceOf(BuildChooserSetting.class), + instanceOf(GitSCMSourceDefaults.class)) + ); BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); assertThat(chooser, notNullValue()); assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); @@ -1986,7 +2023,11 @@ public void given__server_pullMerge_rev_userkey__when__build__then__scmBuilt() t 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)); + assertThat(actual.getExtensions(), containsInAnyOrder( + instanceOf(MergeWithGitSCMExtension.class), + instanceOf(BuildChooserSetting.class), + instanceOf(GitSCMSourceDefaults.class)) + ); BuildChooserSetting chooser = getExtension(actual, BuildChooserSetting.class); assertThat(chooser, notNullValue()); assertThat(chooser.getBuildChooser(), instanceOf(AbstractGitSCMSource.SpecificRevisionBuildChooser.class)); @@ -2061,7 +2102,10 @@ public void given__server_pullMerge_norev_anon__when__build__then__scmBuilt() th 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)); + assertThat(actual.getExtensions(), containsInAnyOrder( + instanceOf(MergeWithGitSCMExtension.class), + instanceOf(GitSCMSourceDefaults.class)) + ); MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); assertThat(merge, notNullValue()); assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); @@ -2126,7 +2170,10 @@ public void given__server_pullMerge_norev_userpass__when__build__then__scmBuilt( 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)); + assertThat(actual.getExtensions(), containsInAnyOrder( + instanceOf(MergeWithGitSCMExtension.class), + instanceOf(GitSCMSourceDefaults.class)) + ); MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); assertThat(merge, notNullValue()); assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); @@ -2191,7 +2238,10 @@ public void given__server_pullMerge_norev_userkey__when__build__then__scmBuilt() 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)); + assertThat(actual.getExtensions(), containsInAnyOrder( + instanceOf(MergeWithGitSCMExtension.class), + instanceOf(GitSCMSourceDefaults.class)) + ); MergeWithGitSCMExtension merge = getExtension(actual, MergeWithGitSCMExtension.class); assertThat(merge, notNullValue()); assertThat(merge.getBaseName(), is("remotes/upstream/test-branch")); diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketHgSCMBuilderTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketHgSCMBuilderTest.java index bb3ee40b4..edc1ee959 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketHgSCMBuilderTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketHgSCMBuilderTest.java @@ -40,7 +40,7 @@ public class BitbucketHgSCMBuilderTest { @Before public void setUp() throws IOException { owner = j.createProject(WorkflowMultiBranchProject.class); - source = new BitbucketSCMSource("test", "tester", "test-repo"); + source = new BitbucketSCMSource("tester", "test-repo"); owner.setSourcesList(Collections.singletonList(new BranchSource(source))); source.setOwner(owner); SystemCredentialsProvider.getInstance().setDomainCredentialsMap(Collections.singletonMap(Domain.global(), diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest.java index aa7d0f09a..b62cb0a56 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceTest.java @@ -393,14 +393,14 @@ public void use_agent_checkout() throws Exception { @Test public void given__instance__when__setTraits_empty__then__traitsEmpty() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("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"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList(new BranchDiscoveryTrait(1), new WebhookRegistrationTrait(WebhookRegistration.DISABLE))); assertThat(instance.getTraits(), @@ -420,35 +420,35 @@ public void given__instance__when__setTraits__then__traitsSet() { @Test public void given__instance__when__setServerUrl__then__urlNormalized() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("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"); + BitbucketSCMSource instance = new BitbucketSCMSource( "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"); + BitbucketSCMSource instance = new BitbucketSCMSource("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"); + BitbucketSCMSource instance = new BitbucketSCMSource("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"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setBitbucketServerUrl(null); assertThat(instance.getServerUrl(), is("https://bitbucket.org")); assertThat(instance.getBitbucketServerUrl(), is(nullValue())); @@ -456,7 +456,7 @@ public void given__instance__when__setBitbucketServerUrl_null__then__cloudUrlApp @Test public void given__instance__when__setBitbucketServerUrl_value__then__valueApplied() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setBitbucketServerUrl("https://bitbucket.test"); assertThat(instance.getServerUrl(), is("https://bitbucket.test")); assertThat(instance.getBitbucketServerUrl(), is("https://bitbucket.test")); @@ -464,7 +464,7 @@ public void given__instance__when__setBitbucketServerUrl_value__then__valueAppli @Test public void given__instance__when__setBitbucketServerUrl_value__then__valueNormalized() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setBitbucketServerUrl("https://bitbucket.test/foo/bar/../../"); assertThat(instance.getServerUrl(), is("https://bitbucket.test")); assertThat(instance.getBitbucketServerUrl(), is("https://bitbucket.test")); @@ -472,7 +472,7 @@ public void given__instance__when__setBitbucketServerUrl_value__then__valueNorma @Test public void given__instance__when__setBitbucketServerUrl_cloudUrl__then__valueApplied() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setBitbucketServerUrl("https://bitbucket.org"); assertThat(instance.getServerUrl(), is("https://bitbucket.org")); assertThat(instance.getBitbucketServerUrl(), is(nullValue())); @@ -480,7 +480,7 @@ public void given__instance__when__setBitbucketServerUrl_cloudUrl__then__valueAp @Test public void given__legacyCode__when__setAutoRegisterHook_true__then__traitAdded() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList( new BranchDiscoveryTrait(true, false), new SSHCheckoutTrait("dummy"))); @@ -495,7 +495,7 @@ public void given__legacyCode__when__setAutoRegisterHook_true__then__traitAdded( @Test public void given__legacyCode__when__setAutoRegisterHook_changes__then__traitUpdated() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList(new BranchDiscoveryTrait(true, false), new SSHCheckoutTrait("dummy"))); assertThat(instance.isAutoRegisterHook(), is(true)); @@ -510,7 +510,7 @@ public void given__legacyCode__when__setAutoRegisterHook_changes__then__traitUpd @Test public void given__legacyCode__when__setAutoRegisterHook_false__then__traitAdded() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList(new BranchDiscoveryTrait(true, false), new SSHCheckoutTrait("dummy"), new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); assertThat(instance.isAutoRegisterHook(), is(true)); @@ -525,7 +525,7 @@ public void given__legacyCode__when__setAutoRegisterHook_false__then__traitAdded @Test public void given__legacyCode__when__setCheckoutCredentials_SAME__then__noTraitAdded() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList( new BranchDiscoveryTrait(true, false), new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); @@ -538,7 +538,7 @@ public void given__legacyCode__when__setCheckoutCredentials_SAME__then__noTraitA @Test public void given__legacyCode__when__setCheckoutCredentials_SAME__then__traitRemoved() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList( new BranchDiscoveryTrait(true, false), new WebhookRegistrationTrait(WebhookRegistration.SYSTEM), @@ -555,7 +555,7 @@ public void given__legacyCode__when__setCheckoutCredentials_SAME__then__traitRem @Test public void given__legacyCode__when__setCheckoutCredentials_null__then__noTraitAdded() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList( new BranchDiscoveryTrait(true, false), new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); @@ -568,7 +568,7 @@ public void given__legacyCode__when__setCheckoutCredentials_null__then__noTraitA @Test public void given__legacyCode__when__setCheckoutCredentials_null__then__traitRemoved() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList( new BranchDiscoveryTrait(true, false), new WebhookRegistrationTrait(WebhookRegistration.SYSTEM), @@ -585,7 +585,7 @@ public void given__legacyCode__when__setCheckoutCredentials_null__then__traitRem @Test public void given__legacyCode__when__setCheckoutCredentials_value__then__traitAdded() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList( new BranchDiscoveryTrait(true, false), new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); @@ -601,7 +601,7 @@ public void given__legacyCode__when__setCheckoutCredentials_value__then__traitAd @Test public void given__legacyCode__when__setCheckoutCredentials_value__then__traitUpdated() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList( new BranchDiscoveryTrait(true, false), new WebhookRegistrationTrait(WebhookRegistration.SYSTEM), @@ -621,7 +621,7 @@ public void given__legacyCode__when__setCheckoutCredentials_value__then__traitUp @Test public void given__legacyCode__when__setCheckoutCredentials_ANONYMOUS__then__traitAdded() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList( new BranchDiscoveryTrait(true, false), new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); @@ -637,7 +637,7 @@ public void given__legacyCode__when__setCheckoutCredentials_ANONYMOUS__then__tra @Test public void given__legacyCode__when__setCheckoutCredentials_ANONYMOUS__then__traitUpdated() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList( new BranchDiscoveryTrait(true, false), new WebhookRegistrationTrait(WebhookRegistration.SYSTEM), @@ -657,7 +657,7 @@ public void given__legacyCode__when__setCheckoutCredentials_ANONYMOUS__then__tra @Test public void given__legacyCode_withoutExcludes__when__setIncludes_default__then__traitRemoved() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList( new BranchDiscoveryTrait(true, false), new WildcardSCMHeadFilterTrait("feature/*", ""), @@ -679,7 +679,7 @@ public void given__legacyCode_withoutExcludes__when__setIncludes_default__then__ @Test public void given__legacyCode_withoutExcludes__when__setIncludes_value__then__traitUpdated() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList( new BranchDiscoveryTrait(true, false), new WildcardSCMHeadFilterTrait("feature/*", ""), @@ -703,7 +703,7 @@ public void given__legacyCode_withoutExcludes__when__setIncludes_value__then__tr @Test public void given__legacyCode_withoutTrait__when__setIncludes_value__then__traitAdded() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList( new BranchDiscoveryTrait(true, false), new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); @@ -724,7 +724,7 @@ public void given__legacyCode_withoutTrait__when__setIncludes_value__then__trait @Test public void given__legacyCode_withExcludes__when__setIncludes_default__then__traitUpdated() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList( new BranchDiscoveryTrait(true, false), new WildcardSCMHeadFilterTrait("feature/*", "feature/ignore"), @@ -748,7 +748,7 @@ public void given__legacyCode_withExcludes__when__setIncludes_default__then__tra @Test public void given__legacyCode_withExcludes__when__setIncludes_value__then__traitUpdated() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList( new BranchDiscoveryTrait(true, false), new WildcardSCMHeadFilterTrait("feature/*", "feature/ignore"), @@ -772,7 +772,7 @@ public void given__legacyCode_withExcludes__when__setIncludes_value__then__trait @Test public void given__legacyCode_withoutIncludes__when__setExcludes_default__then__traitRemoved() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList( new BranchDiscoveryTrait(true, false), new WildcardSCMHeadFilterTrait("*", "feature/ignore"), @@ -794,7 +794,7 @@ public void given__legacyCode_withoutIncludes__when__setExcludes_default__then__ @Test public void given__legacyCode_withoutIncludes__when__setExcludes_value__then__traitUpdated() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList( new BranchDiscoveryTrait(true, false), new WildcardSCMHeadFilterTrait("*", "feature/ignore"), @@ -818,7 +818,7 @@ public void given__legacyCode_withoutIncludes__when__setExcludes_value__then__tr @Test public void given__legacyCode_withoutTrait__when__setExcludes_value__then__traitAdded() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList( new BranchDiscoveryTrait(true, false), new WebhookRegistrationTrait(WebhookRegistration.SYSTEM))); @@ -839,7 +839,7 @@ public void given__legacyCode_withoutTrait__when__setExcludes_value__then__trait @Test public void given__legacyCode_withIncludes__when__setExcludes_default__then__traitUpdated() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList( new BranchDiscoveryTrait(true, false), new WildcardSCMHeadFilterTrait("feature/*", "feature/ignore"), @@ -863,7 +863,7 @@ public void given__legacyCode_withIncludes__when__setExcludes_default__then__tra @Test public void given__legacyCode_withIncludes__when__setExcludes_value__then__traitUpdated() { - BitbucketSCMSource instance = new BitbucketSCMSource("test", "testing", "test-repo"); + BitbucketSCMSource instance = new BitbucketSCMSource("testing", "test-repo"); instance.setTraits(Arrays.asList( new BranchDiscoveryTrait(true, false), new WildcardSCMHeadFilterTrait("feature/*", ""), 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 5a127ff74..a5f4858ca 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchScanningIntegrationTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchScanningIntegrationTest.java @@ -44,6 +44,8 @@ import hudson.model.TopLevelItem; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; import java.util.List; import jenkins.branch.Branch; import jenkins.branch.BranchProjectFactory; @@ -53,6 +55,7 @@ import jenkins.branch.MultiBranchProjectDescriptor; import jenkins.scm.api.SCMSource; import jenkins.scm.api.SCMSourceCriteria; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; import org.acegisecurity.Authentication; import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; import org.junit.Rule; @@ -77,7 +80,15 @@ public void indexingTest() throws Exception { 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"); + BitbucketSCMSource source = new BitbucketSCMSource("amuniz", "test-repos"); + source.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, true), + new OriginPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.HEAD)), + new ForkPullRequestDiscoveryTrait( + EnumSet.of(ChangeRequestCheckoutStrategy.HEAD), + new ForkPullRequestDiscoveryTrait.TrustTeamForks() + ) + )); source.setServerUrl("http://bitbucket.test"); source.setOwner(p); p.getSourcesList().add(new BranchSource(source, new DefaultBranchPropertyStrategy(null))); @@ -92,7 +103,15 @@ public void indexingTest() throws Exception { @Test public void uriResolverByCredentialsTest() throws Exception { WorkflowMultiBranchProject context = j.jenkins.createProject(WorkflowMultiBranchProject.class, "context"); - BitbucketSCMSource source = new BitbucketSCMSource(null, "amuniz", "test-repos"); + BitbucketSCMSource source = new BitbucketSCMSource("amuniz", "test-repos"); + source.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, true), + new OriginPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.HEAD)), + new ForkPullRequestDiscoveryTrait( + EnumSet.of(ChangeRequestCheckoutStrategy.HEAD), + new ForkPullRequestDiscoveryTrait.TrustTeamForks() + ) + )); source.setServerUrl("http://bitbucket.test"); context.getSourcesList().add(new BranchSource(source)); IdCredentials c = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, null, null, "user", "pass"); 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 861bb89e1..c9e51d6fb 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchScanningTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BranchScanningTest.java @@ -33,6 +33,8 @@ import hudson.scm.SCM; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; import java.util.List; import jenkins.plugins.git.AbstractGitSCMSource.SCMRevisionImpl; import jenkins.scm.api.SCMHead; @@ -41,6 +43,7 @@ import jenkins.scm.api.SCMSource; import jenkins.scm.api.SCMSourceCriteria; import jenkins.scm.api.SCMSourceOwner; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; import org.hamcrest.Matchers; import org.junit.Before; import org.junit.ClassRule; @@ -160,7 +163,15 @@ private BitbucketSCMSource getBitbucketSCMSourceMock(BitbucketRepositoryType typ BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, mock); - BitbucketSCMSource source = new BitbucketSCMSource(null, "amuniz", "test-repos"); + BitbucketSCMSource source = new BitbucketSCMSource("amuniz", "test-repos"); + source.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, true), + new OriginPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.HEAD)), + new ForkPullRequestDiscoveryTrait( + EnumSet.of(ChangeRequestCheckoutStrategy.HEAD), + new ForkPullRequestDiscoveryTrait.TrustTeamForks() + ) + )); source.setOwner(getSCMSourceOwnerMock()); return source; } diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTraitTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTraitTest.java index bab72e674..8bade8d07 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTraitTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTraitTest.java @@ -34,7 +34,7 @@ public void given__legacyConfig__when__creatingTrait__then__convertedToModern() 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 BitbucketGitSCMBuilder(new BitbucketSCMSource("example", "does-not-exist"), new BranchSCMHead("master", BitbucketRepositoryType.GIT), null, "scanId"); assumeThat(probe.credentialsId(), is("scanId")); instance.decorateBuilder(probe); @@ -45,7 +45,7 @@ public void given__sshCheckoutWithCredentials__when__decoratingGit__then__creden 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 BitbucketGitSCMBuilder(new BitbucketSCMSource( "example", "does-not-exist"), new BranchSCMHead("master", BitbucketRepositoryType.GIT), null, "scanId"); assumeThat(probe.credentialsId(), is("scanId")); instance.decorateBuilder(probe); @@ -56,7 +56,7 @@ public void given__sshCheckoutWithAgentKey__when__decoratingGit__then__useAgentK 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 BitbucketHgSCMBuilder(new BitbucketSCMSource( "example", "does-not-exist"), new BranchSCMHead("master", BitbucketRepositoryType.MERCURIAL), null, "scanId"); assumeThat(probe.credentialsId(), is("scanId")); instance.decorateBuilder(probe); @@ -67,7 +67,7 @@ public void given__sshCheckoutWithCredentials__when__decoratingHg__then__credent 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 BitbucketHgSCMBuilder(new BitbucketSCMSource( "example", "does-not-exist"), new BranchSCMHead("master", BitbucketRepositoryType.MERCURIAL), null, "scanId"); assumeThat(probe.credentialsId(), is("scanId")); instance.decorateBuilder(probe); 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 f7aa0ecf1..0ce74f4c4 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/WebhooksAutoregisterTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/WebhooksAutoregisterTest.java @@ -57,7 +57,7 @@ public void registerHookTest() throws Exception { RingBufferLogHandler log = createJULTestHandler(); MultiBranchProjectImpl p = j.jenkins.createProject(MultiBranchProjectImpl.class, "test"); - BitbucketSCMSource source = new BitbucketSCMSource(null, "amuniz", "test-repos"); + BitbucketSCMSource source = new BitbucketSCMSource( "amuniz", "test-repos"); source.setAutoRegisterHook(true); p.getSourcesList().add(new BranchSource(source, new DefaultBranchPropertyStrategy(null))); p.scheduleBuild2(0); @@ -79,7 +79,7 @@ public void registerHookTest2() throws Exception { RingBufferLogHandler log = createJULTestHandler(); MultiBranchProjectImpl p = j.jenkins.createProject(MultiBranchProjectImpl.class, "test"); - BitbucketSCMSource source = new BitbucketSCMSource(null, "amuniz", "test-repos"); + BitbucketSCMSource source = new BitbucketSCMSource( "amuniz", "test-repos"); p.getSourcesList().add(new BranchSource(source)); p.scheduleBuild2(0); waitForLogFileMessage("Can not register hook. Jenkins root URL is not valid", log); diff --git a/src/test/java/integration/ScanningFailuresTest.java b/src/test/java/integration/ScanningFailuresTest.java index 283e2102c..d9cb4148e 100644 --- a/src/test/java/integration/ScanningFailuresTest.java +++ b/src/test/java/integration/ScanningFailuresTest.java @@ -2,6 +2,9 @@ import com.cloudbees.jenkins.plugins.bitbucket.BitbucketMockApiFactory; import com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource; +import com.cloudbees.jenkins.plugins.bitbucket.BranchDiscoveryTrait; +import com.cloudbees.jenkins.plugins.bitbucket.ForkPullRequestDiscoveryTrait; +import com.cloudbees.jenkins.plugins.bitbucket.OriginPullRequestDiscoveryTrait; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBranch; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketCommit; @@ -12,7 +15,9 @@ import hudson.model.Result; import hudson.model.TopLevelItem; import java.io.IOException; +import java.util.Arrays; import java.util.Collections; +import java.util.EnumSet; import java.util.List; import java.util.Random; import java.util.concurrent.Callable; @@ -20,6 +25,7 @@ import jenkins.branch.Branch; import jenkins.branch.BranchSource; import jenkins.plugins.git.GitSampleRepoRule; +import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; import org.apache.commons.io.FileUtils; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; @@ -143,7 +149,16 @@ private void getBranchesFails(Callable exception, Result expectedResu BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, api); WorkflowMultiBranchProject mp = j.jenkins.createProject(WorkflowMultiBranchProject.class, "smokes"); - mp.getSourcesList().add(new BranchSource(new BitbucketSCMSource(null, "bob", "foo"))); + BitbucketSCMSource source = new BitbucketSCMSource("bob", "foo"); + source.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, true), + new OriginPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.HEAD)), + new ForkPullRequestDiscoveryTrait( + EnumSet.of(ChangeRequestCheckoutStrategy.HEAD), + new ForkPullRequestDiscoveryTrait.TrustTeamForks() + ) + )); + mp.getSourcesList().add(new BranchSource(source)); mp.scheduleBuild2(0).getFuture().get(); assertThat(mp.getIndexing().getResult(), is(Result.SUCCESS)); @@ -209,7 +224,16 @@ public void checkPathExistsFails() throws Exception { BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, api); WorkflowMultiBranchProject mp = j.jenkins.createProject(WorkflowMultiBranchProject.class, "smokes"); - mp.getSourcesList().add(new BranchSource(new BitbucketSCMSource(null, "bob", "foo"))); + BitbucketSCMSource source = new BitbucketSCMSource("bob", "foo"); + source.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, true), + new OriginPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.HEAD)), + new ForkPullRequestDiscoveryTrait( + EnumSet.of(ChangeRequestCheckoutStrategy.HEAD), + new ForkPullRequestDiscoveryTrait.TrustTeamForks() + ) + )); + mp.getSourcesList().add(new BranchSource(source)); mp.scheduleBuild2(0).getFuture().get(); assertThat(mp.getIndexing().getResult(), is(Result.SUCCESS)); @@ -267,7 +291,16 @@ public void resolveCommitFails() throws Exception { BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, api); WorkflowMultiBranchProject mp = j.jenkins.createProject(WorkflowMultiBranchProject.class, "smokes"); - mp.getSourcesList().add(new BranchSource(new BitbucketSCMSource(null, "bob", "foo"))); + BitbucketSCMSource source = new BitbucketSCMSource("bob", "foo"); + source.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, true), + new OriginPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.HEAD)), + new ForkPullRequestDiscoveryTrait( + EnumSet.of(ChangeRequestCheckoutStrategy.HEAD), + new ForkPullRequestDiscoveryTrait.TrustTeamForks() + ) + )); + mp.getSourcesList().add(new BranchSource(source)); mp.scheduleBuild2(0).getFuture().get(); assertThat(mp.getIndexing().getResult(), is(Result.SUCCESS)); @@ -329,7 +362,16 @@ public void branchRemoved() throws Exception { BitbucketMockApiFactory.add(BitbucketCloudEndpoint.SERVER_URL, api); WorkflowMultiBranchProject mp = j.jenkins.createProject(WorkflowMultiBranchProject.class, "smokes"); - mp.getSourcesList().add(new BranchSource(new BitbucketSCMSource(null, "bob", "foo"))); + BitbucketSCMSource source = new BitbucketSCMSource("bob", "foo"); + source.setTraits(Arrays.asList( + new BranchDiscoveryTrait(true, true), + new OriginPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.HEAD)), + new ForkPullRequestDiscoveryTrait( + EnumSet.of(ChangeRequestCheckoutStrategy.HEAD), + new ForkPullRequestDiscoveryTrait.TrustTeamForks() + ) + )); + mp.getSourcesList().add(new BranchSource(source)); mp.scheduleBuild2(0).getFuture().get(); assertThat(mp.getIndexing().getResult(), is(Result.SUCCESS)); From 255396d264577bbd609a889950ad9c1a9c4508c0 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Fri, 16 Jun 2017 09:31:14 +0100 Subject: [PATCH 21/40] [JENKINS-43507] Pick up latest -SNAPSHOTs --- pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index f110a2372..5fc4590c3 100644 --- a/pom.xml +++ b/pom.xml @@ -51,8 +51,8 @@ 1.642.3 - 2.2.0-SNAPSHOT - 3.4.0-SNAPSHOT + 2.2.0-20170615.114844-13 + 3.4.0-20170616.082946-3 @@ -90,7 +90,7 @@ org.jenkins-ci.plugins mercurial - 2.0-SNAPSHOT + 2.0-20170616.083028-2 org.codehaus.jackson @@ -105,7 +105,7 @@ org.jenkins-ci.plugins branch-api - 2.0.11-SNAPSHOT + 2.0.11-20170615.161903-2 test From 4c80d630a462e8a200c99d711c828c9557d8482a Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Mon, 19 Jun 2017 10:37:35 +0100 Subject: [PATCH 22/40] [JENKINS-43507] Minor formatting tweaks --- .../jenkins/plugins/bitbucket/BitbucketSCMNavigator.java | 2 +- .../plugins/bitbucket/BitbucketSCMNavigatorRequest.java | 6 ++---- .../jenkins/plugins/bitbucket/BitbucketSCMSource.java | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) 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 719cc20ec..653abc5bf 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright (c) 2016, CloudBees, Inc. + * Copyright (c) 2016-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 diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorRequest.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorRequest.java index 801afab0d..6b92d0b7b 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorRequest.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorRequest.java @@ -42,10 +42,8 @@ public class BitbucketSCMNavigatorRequest extends SCMNavigatorRequest { * @param observer the observer. */ protected BitbucketSCMNavigatorRequest(@NonNull SCMNavigator source, - @NonNull - BitbucketSCMNavigatorContext context, - @NonNull - SCMSourceObserver observer) { + @NonNull BitbucketSCMNavigatorContext context, + @NonNull SCMSourceObserver observer) { super(source, context, observer); } } 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 20085f001..ab5152b83 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright (c) 2016, CloudBees, Inc. + * Copyright (c) 2016-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 From e0875fa415ae2fd998518e1a957060b40a3932c6 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Mon, 19 Jun 2017 11:31:38 +0100 Subject: [PATCH 23/40] [JENKINS-43507] Documentation and formatting --- .../bitbucket/BitbucketSCMSourceBuilder.java | 6 +++++- .../plugins/bitbucket/LazyIterable.java | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceBuilder.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceBuilder.java index a0a9a7c58..e8fc04e68 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceBuilder.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceBuilder.java @@ -59,7 +59,7 @@ public class BitbucketSCMSourceBuilder extends SCMSourceBuilder the type of object iterated. + * @since 2.2.0 + */ abstract class LazyIterable implements Iterable { + /** + * The delegate. + */ + @CheckForNull private Iterable delegate; + /** + * Instantiates the delegate. + * + * @return the delegate. + */ + @NonNull protected abstract Iterable create(); + /** + * {@inheritDoc} + */ @Override public synchronized Iterator iterator() { if (delegate == null) { From 9b71dd11bef2798e1aacff728f19ca37e735a26e Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Mon, 19 Jun 2017 21:54:27 +0100 Subject: [PATCH 24/40] [JENKINS-43507] Now is good --- .../jenkins/plugins/bitbucket/BitbucketSCMSource.java | 3 --- 1 file changed, 3 deletions(-) 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 ab5152b83..47eb7da98 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java @@ -722,9 +722,6 @@ public SCMRevision create(@NonNull SCMHead head, @Nullable String hash) + " and branch " + pull.getSource().getBranch().getName()); continue; - } catch (Throwable t) { - // TODO remove - t.printStackTrace(request.listener().getLogger()); } } request.listener().getLogger().format("%n %d pull requests were processed%n", count); From b5f87657e83323a67fdbe8a14c133c47dcc40e5a Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Mon, 19 Jun 2017 22:12:08 +0100 Subject: [PATCH 25/40] [JENKINS-43507] i18n --- .../jenkins/plugins/bitbucket/BranchDiscoveryTrait.java | 2 +- .../com/cloudbees/jenkins/plugins/bitbucket/Messages.properties | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait.java index d0d71ed03..62ad60be7 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait.java @@ -157,7 +157,7 @@ public static class DescriptorImpl extends SCMSourceTraitDescriptor { */ @Override public String getDisplayName() { - return "Discover branches"; + return Messages.BranchDiscoveryTrait_displayName(); } /** 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 3abda79c1..56edf6431 100644 --- a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/Messages.properties +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/Messages.properties @@ -14,6 +14,7 @@ BitbucketRepoMetadataAction.IconDescription.Git=Bitbucket Git Repository BitbucketRepoMetadataAction.IconDescription.Hg=Bitbucket Mercurial Repository BitbucketTeamMetadataAction.IconDescription=Bitbucket Team/Project BranchDiscoveryTrait.allBranches=All branches +BranchDiscoveryTrait.displayName=Discover 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 From 0874e737f9fc9e76a13739346b96cb408945a441 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Mon, 19 Jun 2017 22:12:31 +0100 Subject: [PATCH 26/40] [JENKINS-43507] re-use Throwable.getMessage() --- .../jenkins/plugins/bitbucket/FillErrorResponse.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/FillErrorResponse.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/FillErrorResponse.java index e720a7bb6..5b2d5c1c2 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/FillErrorResponse.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/FillErrorResponse.java @@ -12,11 +12,10 @@ // TODO replace with corresponding core functionality once Jenkins core has JENKINS-42443 class FillErrorResponse extends IOException implements HttpResponse { - private final String message; private final boolean clearList; public FillErrorResponse(String message, boolean clearList) { - this.message = message; + super(message); this.clearList = clearList; } @@ -28,7 +27,7 @@ public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object nod rsp.setHeader("X-Jenkins-Select-Error", clearList ? "clear" : "retain"); rsp.getWriter().print( "
" + Util.escape(message) + + + Jenkins.RESOURCE_PATH + "/images/none.gif\' height=16 width=1>" + Util.escape(getMessage()) + "
"); } From 23b61e58912722aa1c2b92bd2ae9b3fb9adc0a80 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Mon, 19 Jun 2017 22:15:07 +0100 Subject: [PATCH 27/40] [JENKINS-43507] Remove redundant check (only ever called when value is `true` anyway) --- .../plugins/bitbucket/ForkPullRequestDiscoveryTrait.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait.java index cff0ada42..f8e783364 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait.java @@ -329,7 +329,7 @@ public TrustEveryone() { */ @Override protected boolean checkTrusted(@NonNull SCMSourceRequest request, @NonNull ChangeRequestSCMHead2 head) { - return !head.getOrigin().equals(SCMHeadOrigin.DEFAULT); + return true; } /** From c0cfdd784f3b0b82d9757f09e3603d7a86f3061b Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Mon, 19 Jun 2017 22:25:14 +0100 Subject: [PATCH 28/40] [JENKINS-43507] De-duplicate and simplify --- .../client/BitbucketCloudApiClient.java | 71 ++++++------------- 1 file changed, 22 insertions(+), 49 deletions(-) diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java index 6499c6d99..cf6dd3eca 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java @@ -65,7 +65,6 @@ import java.util.logging.Logger; import jenkins.model.Jenkins; import net.sf.json.JSONObject; -import org.apache.commons.httpclient.Header; import org.apache.commons.httpclient.HostConfiguration; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpMethod; @@ -425,8 +424,7 @@ public boolean isPrivate() throws IOException, InterruptedException { return getRepository().isPrivate(); } - private BitbucketRepositoryHooks parsePaginatedRepositoryHooks(String response) throws - IOException { + private BitbucketRepositoryHooks parsePaginatedRepositoryHooks(String response) throws IOException { ObjectMapper mapper = new ObjectMapper(); BitbucketRepositoryHooks parsedResponse; parsedResponse = mapper.readValue(response, BitbucketRepositoryHooks.class); @@ -586,22 +584,7 @@ private String getRequest(String path) throws IOException, InterruptedException GetMethod httpget = new GetMethod(path); try { executeMethod(client, httpget); - String response; - long len = httpget.getResponseContentLength(); - if (len == 0) { - response = ""; - } else { - ByteArrayOutputStream buf; - if (len > 0 && len <= Integer.MAX_VALUE / 2) { - buf = new ByteArrayOutputStream((int) len); - } else { - buf = new ByteArrayOutputStream(); - } - try (InputStream is = httpget.getResponseBodyAsStream()) { - IOUtils.copy(is, buf); - } - response = new String(buf.toByteArray(), StandardCharsets.UTF_8); - } + String response = getResponseContent(httpget, httpget.getResponseContentLength()); if (httpget.getStatusCode() == HttpStatus.SC_NOT_FOUND) { throw new FileNotFoundException("URL: " + path); } @@ -669,36 +652,7 @@ private String postRequest(PostMethod httppost) throws IOException { // 204, no content return ""; } - String response; - long len = -1L; - Header[] headers = httppost.getResponseHeaders("Content-Length"); - if (headers != null && headers.length > 0) { - int i = headers.length - 1; - len = -1L; - while (i >= 0) { - Header header = headers[i]; - try { - len = Long.parseLong(header.getValue()); - break; - } catch (NumberFormatException var5) { - --i; - } - } - } - if (len == 0) { - response = ""; - } else { - ByteArrayOutputStream buf; - if (len > 0 && len <= Integer.MAX_VALUE / 2) { - buf = new ByteArrayOutputStream((int) len); - } else { - buf = new ByteArrayOutputStream(); - } - try (InputStream is = httppost.getResponseBodyAsStream()) { - IOUtils.copy(is, buf); - } - response = new String(buf.toByteArray(), StandardCharsets.UTF_8); - } + String response = getResponseContent(httppost, httppost.getResponseContentLength()); if (httppost.getStatusCode() != HttpStatus.SC_OK && httppost.getStatusCode() != HttpStatus.SC_CREATED) { throw new BitbucketRequestException(httppost.getStatusCode(), "HTTP request error. Status: " + httppost.getStatusCode() + ": " + httppost.getStatusText() + ".\n" + response); } @@ -717,6 +671,25 @@ private String postRequest(PostMethod httppost) throws IOException { } + private String getResponseContent(HttpMethod httppost, long len) throws IOException { + String response; + if (len == 0) { + response = ""; + } else { + ByteArrayOutputStream buf; + if (len > 0 && len <= Integer.MAX_VALUE / 2) { + buf = new ByteArrayOutputStream((int) len); + } else { + buf = new ByteArrayOutputStream(); + } + try (InputStream is = httppost.getResponseBodyAsStream()) { + IOUtils.copy(is, buf); + } + response = new String(buf.toByteArray(), StandardCharsets.UTF_8); + } + return response; + } + private String serialize(T o) throws IOException { ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(o); From e1b6547605b204976ac226ce705e7ee279abc844 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Mon, 19 Jun 2017 22:25:35 +0100 Subject: [PATCH 29/40] [JENKINS-43507] Remove potential user error --- .../plugins/bitbucket/endpoints/BitbucketCloudEndpoint.java | 4 ++++ .../bitbucket/endpoints/BitbucketEndpointConfiguration.java | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpoint.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpoint.java index 360ee673f..40d2a9182 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpoint.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpoint.java @@ -42,6 +42,10 @@ public class BitbucketCloudEndpoint extends AbstractBitbucketEndpoint { * The URL of Bitbucket Cloud. */ public static final String SERVER_URL = "https://bitbucket.org"; + /** + * A bad URL of Bitbucket Cloud. + */ + public static final String BAD_SERVER_URL = "http://bitbucket.org"; /** * Constructor. diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java index bb9cf3688..b3785e520 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java @@ -92,7 +92,8 @@ public String readResolveServerUrl(@CheckForNull String bitbucketServerUrl) { String serverUrl = normalizeServerUrl(bitbucketServerUrl); AbstractBitbucketEndpoint endpoint = findEndpoint(serverUrl); if (endpoint == null && ACL.SYSTEM.equals(Jenkins.getAuthentication())) { - if (BitbucketCloudEndpoint.SERVER_URL.equals(serverUrl)) { + if (BitbucketCloudEndpoint.SERVER_URL.equals(serverUrl) + || BitbucketCloudEndpoint.BAD_SERVER_URL.equals(serverUrl)) { // exception case addEndpoint(new BitbucketCloudEndpoint(false, null)); } else { From 103082f85d2099fa6da9be2b16771da8b0b046e3 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Mon, 19 Jun 2017 22:31:24 +0100 Subject: [PATCH 30/40] [JENKINS-43507] Fix formatting --- .../plugins/bitbucket/BitbucketSCMNavigator/config.jelly | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 3211612b3..7316a8bcb 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 @@ -14,8 +14,8 @@ - - + + From 107f7474fbf05686c57ddb6350ddb3a3be94f334 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Tue, 20 Jun 2017 13:48:26 +0100 Subject: [PATCH 31/40] [JENKINS-43507] Pick up alpha-1 --- pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 5fc4590c3..e79f64f8b 100644 --- a/pom.xml +++ b/pom.xml @@ -51,8 +51,8 @@ 1.642.3 - 2.2.0-20170615.114844-13 - 3.4.0-20170616.082946-3 + 2.2.0-alpha-1 + 3.4.0-alpha-1 @@ -90,7 +90,7 @@ org.jenkins-ci.plugins mercurial - 2.0-20170616.083028-2 + 2.0-alpha-1 org.codehaus.jackson @@ -105,7 +105,7 @@ org.jenkins-ci.plugins branch-api - 2.0.11-20170615.161903-2 + 2.0.11-alpha-1 test From 6821f23ae993e599f19673738667c920053b9dd9 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Tue, 20 Jun 2017 13:52:43 +0100 Subject: [PATCH 32/40] [maven-release-plugin] prepare release cloudbees-bitbucket-branch-source-2.2.0-alpha-1 --- pom.xml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index e79f64f8b..98f053496 100644 --- a/pom.xml +++ b/pom.xml @@ -22,19 +22,18 @@ ~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ~ THE SOFTWARE. --> - + 4.0.0 org.jenkins-ci.plugins plugin 2.22 - + cloudbees-bitbucket-branch-source - 2.2.0-SNAPSHOT + 2.2.0-alpha-1 hpi Bitbucket Branch Source Plugin @@ -59,7 +58,7 @@ scm:git:git://github.com/jenkinsci/bitbucket-branch-source-plugin.git scm:git:git@github.com:jenkinsci/bitbucket-branch-source-plugin.git https://github.com/jenkinsci/bitbucket-branch-source-plugin - HEAD + cloudbees-bitbucket-branch-source-2.2.0-alpha-1 From 8b3b9eb920a16d70db9a824c59acaaa697ca0511 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Tue, 20 Jun 2017 13:52:43 +0100 Subject: [PATCH 33/40] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 98f053496..9dcfa76b0 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ cloudbees-bitbucket-branch-source - 2.2.0-alpha-1 + 2.2.0-SNAPSHOT hpi Bitbucket Branch Source Plugin @@ -58,7 +58,7 @@ scm:git:git://github.com/jenkinsci/bitbucket-branch-source-plugin.git scm:git:git@github.com:jenkinsci/bitbucket-branch-source-plugin.git https://github.com/jenkinsci/bitbucket-branch-source-plugin - cloudbees-bitbucket-branch-source-2.2.0-alpha-1 + HEAD
From 17541eb1a4be3744d684e8d7809d4af1d884cc50 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Tue, 20 Jun 2017 16:35:54 +0100 Subject: [PATCH 34/40] [JENKINS-43507] US people want to spell things incorrectly --- .../plugins/bitbucket/BitbucketSCMNavigator/config_en.properties | 1 + .../bitbucket/BitbucketSCMNavigator/config_en_US.properties | 1 + .../bitbucket/BitbucketSCMSource/config-detail_en.properties | 1 + .../bitbucket/BitbucketSCMSource/config-detail_en_US.properties | 1 + 4 files changed, 4 insertions(+) create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/config_en.properties create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/config_en_US.properties create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/config-detail_en.properties create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/config-detail_en_US.properties diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/config_en.properties b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/config_en.properties new file mode 100644 index 000000000..e337e9c00 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/config_en.properties @@ -0,0 +1 @@ +Behaviours=Behaviours diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/config_en_US.properties b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/config_en_US.properties new file mode 100644 index 000000000..4155e0949 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator/config_en_US.properties @@ -0,0 +1 @@ +Behaviours=Behaviors diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/config-detail_en.properties b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/config-detail_en.properties new file mode 100644 index 000000000..e337e9c00 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/config-detail_en.properties @@ -0,0 +1 @@ +Behaviours=Behaviours diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/config-detail_en_US.properties b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/config-detail_en_US.properties new file mode 100644 index 000000000..4155e0949 --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource/config-detail_en_US.properties @@ -0,0 +1 @@ +Behaviours=Behaviors From 3fa5f43ab46ab2bb7197901a1e426cf91c64dd3c Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Sat, 24 Jun 2017 08:43:59 +0100 Subject: [PATCH 35/40] [JENKINS-43507] Fix trait drop-down list population --- .../plugins/bitbucket/BitbucketSCMNavigator.java | 9 ++++++--- .../plugins/bitbucket/BitbucketSCMSource.java | 6 ++++-- .../plugins/bitbucket/BranchDiscoveryTrait.java | 13 +++++++++++-- .../bitbucket/ForkPullRequestDiscoveryTrait.java | 13 +++++++++++-- .../bitbucket/OriginPullRequestDiscoveryTrait.java | 13 +++++++++++-- .../bitbucket/PublicRepoPullRequestFilterTrait.java | 10 ++++++++++ .../jenkins/plugins/bitbucket/SSHCheckoutTrait.java | 13 +++++++++++-- .../plugins/bitbucket/WebhookRegistrationTrait.java | 13 +++++++++++-- 8 files changed, 75 insertions(+), 15 deletions(-) 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 653abc5bf..a3a222433 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java @@ -69,6 +69,7 @@ import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import jenkins.model.Jenkins; import jenkins.plugins.git.traits.GitBrowserSCMSourceTrait; import jenkins.scm.api.SCMNavigator; import jenkins.scm.api.SCMNavigatorDescriptor; @@ -631,12 +632,14 @@ public ListBoxModel doFillCredentialsIdItems(@AncestorInPath SCMSourceOwner cont } public List>> getTraitsDescriptorLists() { + BitbucketSCMSource.DescriptorImpl sourceDescriptor = + Jenkins.getActiveInstance().getDescriptorByType(BitbucketSCMSource.DescriptorImpl.class); 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)); + all.addAll(SCMSourceTrait._for(sourceDescriptor, BitbucketSCMSourceContext.class, null)); + all.addAll(SCMSourceTrait._for(sourceDescriptor, null, BitbucketGitSCMBuilder.class)); + all.addAll(SCMSourceTrait._for(sourceDescriptor, null, BitbucketHgSCMBuilder.class)); Set> dedup = new HashSet<>(); for (Iterator> iterator = all.iterator(); iterator.hasNext(); ) { SCMTraitDescriptor d = iterator.next(); 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 47eb7da98..97964186c 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java @@ -1216,8 +1216,10 @@ protected SCMHeadCategory[] createCategories() { } public List> getTraitsDescriptorLists() { - List all = - SCMSourceTrait._for(this, BitbucketSCMSourceContext.class, null); + List all = new ArrayList<>(); + // all that are applicable to our context + all.addAll(SCMSourceTrait._for(this, BitbucketSCMSourceContext.class, null)); + // all that are applicable to our builders all.addAll(SCMSourceTrait._for(this, null, BitbucketGitSCMBuilder.class)); all.addAll(SCMSourceTrait._for(this, null, BitbucketHgSCMBuilder.class)); Set dedup = new HashSet<>(); diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait.java index 62ad60be7..907ca0b1c 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait.java @@ -32,6 +32,7 @@ import jenkins.scm.api.SCMHeadCategory; import jenkins.scm.api.SCMHeadOrigin; import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMSource; import jenkins.scm.api.trait.SCMHeadAuthority; import jenkins.scm.api.trait.SCMHeadAuthorityDescriptor; import jenkins.scm.api.trait.SCMHeadFilter; @@ -164,8 +165,16 @@ public String getDisplayName() { * {@inheritDoc} */ @Override - public boolean isApplicableToContext(@NonNull Class contextClass) { - return BitbucketSCMSourceContext.class.isAssignableFrom(contextClass); + public Class getContextClass() { + return BitbucketSCMSourceContext.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getSourceClass() { + return BitbucketSCMSource.class; } /** diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait.java index f8e783364..15e1a0156 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait.java @@ -34,6 +34,7 @@ import jenkins.scm.api.SCMHeadCategory; import jenkins.scm.api.SCMHeadOrigin; import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMSource; import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; import jenkins.scm.api.mixin.ChangeRequestSCMHead2; import jenkins.scm.api.trait.SCMHeadAuthority; @@ -172,8 +173,16 @@ public String getDisplayName() { * {@inheritDoc} */ @Override - public boolean isApplicableToContext(@NonNull Class contextClass) { - return BitbucketSCMSourceContext.class.isAssignableFrom(contextClass); + public Class getContextClass() { + return BitbucketSCMSourceContext.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getSourceClass() { + return BitbucketSCMSource.class; } /** diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait.java index 9fdebca85..b98974b7e 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait.java @@ -31,6 +31,7 @@ import jenkins.scm.api.SCMHeadCategory; import jenkins.scm.api.SCMHeadOrigin; import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMSource; import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; import jenkins.scm.api.mixin.ChangeRequestSCMHead2; import jenkins.scm.api.trait.SCMHeadAuthority; @@ -143,8 +144,16 @@ public String getDisplayName() { * {@inheritDoc} */ @Override - public boolean isApplicableToContext(@NonNull Class contextClass) { - return BitbucketSCMSourceContext.class.isAssignableFrom(contextClass); + public Class getContextClass() { + return BitbucketSCMSourceContext.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getSourceClass() { + return BitbucketSCMSource.class; } /** diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTrait.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTrait.java index 54d789029..c117858bd 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTrait.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTrait.java @@ -26,6 +26,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; +import jenkins.scm.api.SCMSource; import jenkins.scm.api.trait.SCMSourceContext; import jenkins.scm.api.trait.SCMSourceTrait; import jenkins.scm.api.trait.SCMSourceTraitDescriptor; @@ -75,5 +76,14 @@ public String getDisplayName() { public Class getContextClass() { return BitbucketSCMSourceContext.class; } + + /** + * {@inheritDoc} + */ + @Override + public Class getSourceClass() { + return BitbucketSCMSource.class; + } + } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait.java index 2853bbe70..ea336d149 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait.java @@ -43,6 +43,7 @@ import hudson.util.ListBoxModel; import jenkins.model.Jenkins; import jenkins.plugins.git.GitSCMBuilder; +import jenkins.scm.api.SCMSource; import jenkins.scm.api.trait.SCMBuilder; import jenkins.scm.api.trait.SCMSourceContext; import jenkins.scm.api.trait.SCMSourceTrait; @@ -126,8 +127,16 @@ public String getDisplayName() { * {@inheritDoc} */ @Override - public boolean isApplicableToContext(@NonNull Class contextClass) { - return BitbucketSCMSourceContext.class.isAssignableFrom(contextClass); + public Class getContextClass() { + return BitbucketSCMSourceContext.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getSourceClass() { + return BitbucketSCMSource.class; } /** diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait.java index 4549c1f1b..10c1a9f82 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait.java @@ -27,6 +27,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.util.ListBoxModel; +import jenkins.scm.api.SCMSource; import jenkins.scm.api.trait.SCMSourceContext; import jenkins.scm.api.trait.SCMSourceTrait; import jenkins.scm.api.trait.SCMSourceTraitDescriptor; @@ -103,8 +104,16 @@ public String getDisplayName() { * {@inheritDoc} */ @Override - public boolean isApplicableToContext(@NonNull Class contextClass) { - return BitbucketSCMSourceContext.class.isAssignableFrom(contextClass); + public Class getContextClass() { + return BitbucketSCMSourceContext.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getSourceClass() { + return BitbucketSCMSource.class; } /** From b9a30162569aa0abc9fe1110c79adb6d55bbb3aa Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Mon, 26 Jun 2017 12:52:43 +0100 Subject: [PATCH 36/40] [JENKINS-43507] Prepare for -alpha-4 - Pick up updated git and mercurial dependencies - Pick up structs plugin 1.9 - Ensure consistent behaviour for new code --- pom.xml | 9 +++++++-- .../jenkins/plugins/bitbucket/BitbucketSCMNavigator.java | 4 ---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 9dcfa76b0..a8e990c79 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ 1.642.3 2.2.0-alpha-1 - 3.4.0-alpha-1 + 3.4.0-alpha-4 @@ -71,6 +71,11 @@ + + org.jenkins-ci.plugins + structs + 1.9 + org.jenkins-ci.plugins scm-api @@ -89,7 +94,7 @@ org.jenkins-ci.plugins mercurial - 2.0-alpha-1 + 2.0-alpha-4 org.codehaus.jackson 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 a3a222433..0ed9eee9b 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java @@ -149,10 +149,6 @@ public BitbucketSCMNavigator(String repoOwner) { this.repoOwner = repoOwner; this.traits = new ArrayList<>(); this.credentialsId = null; // highlighting the default is anonymous unless you configure explicitly - 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 From 3c9bb0f9b848f5de78fc0b42e8348e7dafb92d67 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Mon, 26 Jun 2017 12:55:30 +0100 Subject: [PATCH 37/40] [maven-release-plugin] prepare release cloudbees-bitbucket-branch-source-2.2.0-alpha-4 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index a8e990c79..5a9c05d95 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ cloudbees-bitbucket-branch-source - 2.2.0-SNAPSHOT + 2.2.0-alpha-4 hpi Bitbucket Branch Source Plugin @@ -58,7 +58,7 @@ scm:git:git://github.com/jenkinsci/bitbucket-branch-source-plugin.git scm:git:git@github.com:jenkinsci/bitbucket-branch-source-plugin.git https://github.com/jenkinsci/bitbucket-branch-source-plugin - HEAD + cloudbees-bitbucket-branch-source-2.2.0-alpha-4
From 6f9a49f0b9471b2375d35715a6693f616d98cd64 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Mon, 26 Jun 2017 12:55:30 +0100 Subject: [PATCH 38/40] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 5a9c05d95..a8e990c79 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ cloudbees-bitbucket-branch-source - 2.2.0-alpha-4 + 2.2.0-SNAPSHOT hpi Bitbucket Branch Source Plugin @@ -58,7 +58,7 @@ scm:git:git://github.com/jenkinsci/bitbucket-branch-source-plugin.git scm:git:git@github.com:jenkinsci/bitbucket-branch-source-plugin.git https://github.com/jenkinsci/bitbucket-branch-source-plugin - cloudbees-bitbucket-branch-source-2.2.0-alpha-4 + HEAD
From 7ecc02dd2aadd4ce6e6126782e6da12f07464983 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Wed, 5 Jul 2017 18:49:25 +0100 Subject: [PATCH 39/40] [JENKINS-43507] Add missing continue --- .../bitbucket/endpoints/BitbucketEndpointConfiguration.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java index b3785e520..a520b7b6a 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java @@ -163,6 +163,7 @@ public synchronized void setEndpoints(@CheckForNull List Date: Wed, 5 Jul 2017 18:59:29 +0100 Subject: [PATCH 40/40] [JENKINS-43507] Remove (now) useless dependency --- pom.xml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pom.xml b/pom.xml index a8e990c79..5bd451a89 100644 --- a/pom.xml +++ b/pom.xml @@ -144,14 +144,6 @@ 2.11 test - - org.jenkins-ci.plugins.workflow - workflow-aggregator - 1.15 - tests - test - -