diff --git a/pom.xml b/pom.xml
index e95457f55..5bd451a89 100644
--- a/pom.xml
+++ b/pom.xml
@@ -23,144 +23,159 @@
~ 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
- 2.1.3-SNAPSHOT
- hpi
+ cloudbees-bitbucket-branch-source
+ 2.2.0-SNAPSHOT
+ 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.0.4
-
+
+ 1.642.3
+ 2.2.0-alpha-1
+ 3.4.0-alpha-4
+
-
- 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
+
+
-
- org.jenkins-ci.plugins
- scm-api
- ${scm-api.version}
-
-
- org.jenkins-ci.plugins
- git
- 2.6.5
-
-
- org.apache.httpcomponents
- httpclient
-
-
-
-
- org.jenkins-ci.plugins
- mercurial
- 1.58
-
-
- 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.6
- 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
- 2.6.5
- 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
+ scm-api
+ ${scm-api.version}
+
+
+
+
+ org.jenkins-ci.plugins
+ structs
+ 1.9
+
+
+ org.jenkins-ci.plugins
+ scm-api
+
+
+ org.jenkins-ci.plugins
+ git
+ ${git.version}
+
+
+ org.apache.httpcomponents
+ httpclient
+
+
+
+
+ org.jenkins-ci.plugins
+ mercurial
+ 2.0-alpha-4
+
+
+ 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-alpha-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
+
+
-
-
- 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
-
- 2.0.0
-
-
-
-
+
+
+
+ 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..22d6b8437
--- /dev/null
+++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketHgSCMBuilder.java
@@ -0,0 +1,264 @@
+/*
+ * 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 java.util.logging.Level;
+import java.util.logging.Logger;
+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 {
+ /**
+ * 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.
+ */
+ @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) {
+ 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
+ }
+ }
+ 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..0ed9eee9b 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
@@ -29,37 +29,77 @@
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.model.Jenkins;
+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 +107,334 @@
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;
}
@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 extends 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 +446,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 extends BitbucketRepository> 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 extends BitbucketRepository> 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 +496,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 +510,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 +578,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 +597,120 @@ 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() {
+ 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(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();
+ 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 +818,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/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..6b92d0b7b
--- /dev/null
+++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigatorRequest.java
@@ -0,0 +1,49 @@
+/*
+ * 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);
+ }
+}
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 d3a229849..97964186c 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
@@ -33,53 +33,65 @@
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;
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;
@@ -89,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;
@@ -108,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.
@@ -166,16 +222,76 @@ 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 repoOwner the repository owner.
+ * @param repository the repository name.
+ * @since 2.2.0
+ */
@DataBoundConstructor
- public BitbucketSCMSource(String id, String repoOwner, 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<>();
+ }
+
+ /**
+ * 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()));
+ }
+
+ /**
+ * 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
@@ -184,103 +300,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();
}
- } else {
- protocol = BitbucketRepositoryProtocol.HTTP;
}
- return buildBitbucketClient().getRepositoryUri(repositoryType, protocol, protocolPortOverride, repoOwner, repository);
+ 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;
+ }
+ }
+ return true;
}
public BitbucketRepositoryType getRepositoryType() throws IOException, InterruptedException {
@@ -296,11 +502,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
@@ -310,8 +516,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);
}
}
@@ -319,239 +524,343 @@ 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 BitbucketApi bitbucket = buildBitbucketClient();
- if (bitbucket.isPrivate()) {
- List extends BitbucketPullRequest> 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);
- if (includes != null && !includes.contains(head)) {
- continue;
- }
-
+ final String fullName = repoOwner + "/" + repository;
- 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;
}
- 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 extends BitbucketBranch> 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 extends BitbucketBranch> 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 extends BitbucketBranch> branches = bitbucket.getBranches();
+ private String findRawNode(String branchName, List extends BitbucketBranch> 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 {
@@ -563,12 +872,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(),
@@ -577,7 +886,7 @@ public SCM build(SCMHead head, SCMRevision revision) {
));
cloneLinks.add(new BitbucketHref("https",
bitbucket.getRepositoryUri(
- repositoryType,
+ type,
BitbucketRepositoryProtocol.HTTP,
null,
getRepoOwner(),
@@ -586,65 +895,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
@@ -653,77 +941,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,
@@ -742,16 +968,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;
}
@@ -764,42 +990,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) {
@@ -831,6 +1056,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 {
@@ -852,6 +1088,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) {
@@ -865,17 +1103,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 extends BitbucketRepository> 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
@@ -887,6 +1214,56 @@ protected SCMHeadCategory[] createCategories() {
// TODO add support for tags and maybe feature branch identification
};
}
+
+ public List> getTraitsDescriptorLists() {
+ 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<>();
+ 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 {
@@ -931,4 +1308,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..e8fc04e68
--- /dev/null
+++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceBuilder.java
@@ -0,0 +1,130 @@
+/*
+ * 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#getServerUrl()}
+ * @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.
+ */
+ @CheckForNull
+ 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.
+ */
+ @NonNull
+ public final String serverUrl() {
+ return serverUrl;
+ }
+
+ /**
+ * The credentials that the {@link BitbucketSCMSource} will use.
+ *
+ * @return the credentials that the {@link BitbucketSCMSource} will use.
+ */
+ @CheckForNull
+ 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.
+ */
+ @NonNull
+ public final String repoOwner() {
+ return repoOwner;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @NonNull
+ @Override
+ public BitbucketSCMSource build() {
+ BitbucketSCMSource result = new BitbucketSCMSource(repoOwner(), projectName());
+ result.setId(id());
+ 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
new file mode 100644
index 000000000..70266a400
--- /dev/null
+++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceContext.java
@@ -0,0 +1,308 @@
+/*
+ * 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;
+ /**
+ * {@code true} if all pull requests from public repositories should be ignored.
+ */
+ private boolean skipPublicPRs;
+ /**
+ * 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 {@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 skipPublicPRs() {
+ return skipPublicPRs;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Controls the skipping of pull requests from public repositories.
+ *
+ * @param skipPublicPRs {@code true} if pull requests from public repositories should be skipped.
+ * @return {@code this} for method chaining.
+ */
+ public final BitbucketSCMSourceContext skipPublicPRs(boolean skipPublicPRs) {
+ this.skipPublicPRs = skipPublicPRs;
+ 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..b7f8d7e73
--- /dev/null
+++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSourceRequest.java
@@ -0,0 +1,381 @@
+/*
+ * 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;
+ /**
+ * {@code true} if all pull requests from public repositories should be ignored.
+ */
+ private final boolean skipPublicPRs;
+ /**
+ * 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 final BitbucketSCMSource source,
+ @NonNull BitbucketSCMSourceContext context,
+ @CheckForNull TaskListener listener) {
+ super(source, context, listener);
+ fetchBranches = context.wantBranches();
+ fetchTags = context.wantTags();
+ fetchOriginPRs = context.wantOriginPRs();
+ fetchForkPRs = context.wantForkPRs();
+ skipPublicPRs = context.skipPublicPRs();
+ 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 {@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.
+ *
+ * @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();
+ }
+}
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..907ca0b1c
--- /dev/null
+++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BranchDiscoveryTrait.java
@@ -0,0 +1,280 @@
+/*
+ * 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.SCMSource;
+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.
+ *
+ *
Bit 0
+ *
Build branches that are not filed as a PR
+ *
Bit 1
+ *
Build branches that are filed as a PR
+ *
+ */
+ 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 Messages.BranchDiscoveryTrait_displayName();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Class extends SCMSourceContext> getContextClass() {
+ return BitbucketSCMSourceContext.class;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Class extends SCMSource> getSourceClass() {
+ return BitbucketSCMSource.class;
+ }
+
+ /**
+ * 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 extends SCMHeadOrigin> 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/FillErrorResponse.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/FillErrorResponse.java
new file mode 100644
index 000000000..5b2d5c1c2
--- /dev/null
+++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/FillErrorResponse.java
@@ -0,0 +1,34 @@
+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 boolean clearList;
+
+ public FillErrorResponse(String message, boolean clearList) {
+ super(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(getMessage()) +
+ "
");
+
+ }
+}
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..15e1a0156
--- /dev/null
+++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/ForkPullRequestDiscoveryTrait.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 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 java.util.Set;
+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;
+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 super BitbucketSCMSourceRequest, ? extends
+ ChangeRequestSCMHead2, ? extends SCMRevision> trust) {
+ this.strategyId = strategyId;
+ this.trust = trust;
+ }
+
+ /**
+ * Constructor for programmatic instantiation.
+ *
+ * @param strategies the {@link ChangeRequestCheckoutStrategy} instances.
+ * @param trust the authority.
+ */
+ public ForkPullRequestDiscoveryTrait(@NonNull Set strategies,
+ @NonNull SCMHeadAuthority super BitbucketSCMSourceRequest, ? extends
+ ChangeRequestSCMHead2, ? extends SCMRevision> 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;
+ }
+
+
+ /**
+ * 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.
+ *
+ * @return the authority.
+ */
+ @NonNull
+ public SCMHeadAuthority super BitbucketSCMSourceRequest, ? extends ChangeRequestSCMHead2, ? extends
+ SCMRevision> getTrust() {
+ return trust;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void decorateContext(SCMSourceContext, ?> context) {
+ BitbucketSCMSourceContext ctx = (BitbucketSCMSourceContext) context;
+ ctx.wantForkPRs(true);
+ ctx.withAuthority(trust);
+ ctx.withForkPRStrategies(getStrategies());
+ }
+
+ /**
+ * {@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 Class extends SCMSourceContext> getContextClass() {
+ return BitbucketSCMSourceContext.class;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Class extends SCMSource> getSourceClass() {
+ return BitbucketSCMSource.class;
+ }
+
+ /**
+ * 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 extends SCMHeadOrigin> 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 extends SCMHeadOrigin> 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 true;
+ }
+
+ /**
+ * 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 extends SCMHeadOrigin> 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..b82423a9f
--- /dev/null
+++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/LazyIterable.java
@@ -0,0 +1,61 @@
+/*
+ * 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 java.util.Iterator;
+
+/**
+ * An iterable that will lazily instantiate its delegate.
+ *
+ * @param 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) {
+ 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..b98974b7e
--- /dev/null
+++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/OriginPullRequestDiscoveryTrait.java
@@ -0,0 +1,211 @@
+/*
+ * 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.EnumSet;
+import java.util.Set;
+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;
+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(Set 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;
+ }
+
+ /**
+ * 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}
+ */
+ @Override
+ protected void decorateContext(SCMSourceContext, ?> context) {
+ BitbucketSCMSourceContext ctx = (BitbucketSCMSourceContext) context;
+ ctx.wantOriginPRs(true);
+ ctx.withAuthority(new OriginChangeRequestSCMHeadAuthority());
+ ctx.withOriginPRStrategies(getStrategies());
+ }
+
+ /**
+ * {@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 Class extends SCMSourceContext> getContextClass() {
+ return BitbucketSCMSourceContext.class;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Class extends SCMSource> getSourceClass() {
+ return BitbucketSCMSource.class;
+ }
+
+ /**
+ * 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 extends SCMHeadOrigin> 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..c117858bd
--- /dev/null
+++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/PublicRepoPullRequestFilterTrait.java
@@ -0,0 +1,89 @@
+/*
+ * 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 jenkins.scm.api.SCMSource;
+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.
+ *
+ * @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 extends SCMSourceContext> getContextClass() {
+ return BitbucketSCMSourceContext.class;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Class extends SCMSource> getSourceClass() {
+ return BitbucketSCMSource.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 06afb69b0..116aae865 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.scm.api.mixin.ChangeRequestSCMHead;
+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.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,70 @@ public class PullRequestSCMHead extends SCMHead implements ChangeRequestSCMHead
private final BranchSCMHead target;
- public PullRequestSCMHead(String repoOwner, String repository, String branchName,
- String number, BranchSCMHead target) {
- super(PR_BRANCH_PREFIX + number);
+ private final SCMHeadOrigin origin;
+
+ 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 repoOwner, String repository, String branchName, BitbucketPullRequest pr) {
- this(repoOwner, repository, null, branchName, pr);
- }
-
- public PullRequestSCMHead(String repoOwner, String repository, BitbucketRepositoryType repositoryType, String branchName, BitbucketPullRequest pr) {
- super(PR_BRANCH_PREFIX + pr.getId());
+ 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
+ @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, null);
+ }
+
+ @Deprecated
+ @Restricted(DoNotUse.class)
+ public PullRequestSCMHead(String repoOwner, String repository, BitbucketRepositoryType repositoryType,
+ String branchName, BitbucketPullRequest pr) {
+ this(repoOwner, repository, repositoryType, branchName, pr, null);
+ }
+
+ @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
@@ -80,6 +133,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 || 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;
}
@@ -111,4 +168,121 @@ public SCMHead getTarget() {
return target;
}
+ @NonNull
+ @Override
+ public ChangeRequestCheckoutStrategy getCheckoutStrategy() {
+ return strategy;
+ }
+
+ @NonNull
+ @Override
+ public String getOriginName() {
+ return branchName;
+ }
+
+ @NonNull
+ @Override
+ public SCMHeadOrigin getOrigin() {
+ return origin == null ? SCMHeadOrigin.DEFAULT : origin;
+ }
+
+ /**
+ * Used to handle data migration.
+ *
+ * @see FixLegacyMigration1
+ * @see FixLegacyMigration2
+ * @deprecated used for data migration.
+ */
+ @Deprecated
+ @Restricted(NoExternalUse.class)
+ public static class FixLegacy extends PullRequestSCMHead {
+
+ FixLegacy(PullRequestSCMHead copy) {
+ super(copy.getName(), copy.repoOwner, copy.repository, copy.branchName, copy.number,
+ copy.target, copy.getOrigin(), ChangeRequestCheckoutStrategy.HEAD);
+ }
+ }
+
+ /**
+ * Used to handle data migration.
+ *
+ * @deprecated used for data migration.
+ */
+ @Restricted(NoExternalUse.class)
+ @Extension
+ public static class FixLegacyMigration1 extends
+ SCMHeadMigration {
+ public FixLegacyMigration1() {
+ super(BitbucketSCMSource.class, FixLegacy.class, AbstractGitSCMSource.SCMRevisionImpl.class);
+ }
+
+ @Override
+ 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.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, (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;
+ }
+ }
+
+ /**
+ * Used to handle data migration.
+ *
+ * @deprecated used for data migration.
+ */
+ @Restricted(NoExternalUse.class)
+ @Extension
+ public static class FixLegacyMigration2 extends
+ SCMHeadMigration {
+ public FixLegacyMigration2() {
+ super(BitbucketSCMSource.class, FixLegacy.class, BitbucketSCMSource.MercurialRevision.class);
+ }
+
+ @Override
+ 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.originOf(head.getRepoOwner(), head.getRepository()),
+ ChangeRequestCheckoutStrategy.HEAD
+ );
+ }
+
+ @Override
+ public SCMRevision migrate(@NonNull BitbucketSCMSource source,
+ @NonNull BitbucketSCMSource.MercurialRevision revision) {
+ 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 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 922a22296..56e094bf8 100644
--- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SCMHeadWithOwnerAndRepo.java
+++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SCMHeadWithOwnerAndRepo.java
@@ -41,6 +41,7 @@
import jenkins.scm.api.SCMHead;
import jenkins.scm.api.SCMHeadMigration;
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;
@@ -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);
}
}
@@ -107,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()
);
@@ -135,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) {
@@ -143,16 +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(),
- new BranchSCMHead(target, BitbucketRepositoryType.MERCURIAL));
+ return new PullRequestSCMHead(
+ head.getName(),
+ head.getRepoOwner(),
+ head.getRepository(),
+ head.getBranchName(),
+ head.getId(),
+ new BranchSCMHead(target, BitbucketRepositoryType.MERCURIAL),
+ 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;
}
}
@@ -165,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) {
@@ -173,15 +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(),
- new BranchSCMHead(target, BitbucketRepositoryType.GIT));
+ return new PullRequestSCMHead(
+ head.getName(),
+ head.getRepoOwner(),
+ head.getRepository(),
+ head.getBranchName(),
+ head.getId(),
+ new BranchSCMHead(target, BitbucketRepositoryType.GIT),
+ 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..ea336d149
--- /dev/null
+++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/SSHCheckoutTrait.java
@@ -0,0 +1,191 @@
+/*
+ * 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.SCMSource;
+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 defer to the agent configured
+ * credentials (typically anonymous but not always)
+ */
+ @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 Class extends SCMSourceContext> getContextClass() {
+ return BitbucketSCMSourceContext.class;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Class extends SCMSource> getSourceClass() {
+ return BitbucketSCMSource.class;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isApplicableToBuilder(@NonNull Class extends SCMBuilder> 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..10c1a9f82
--- /dev/null
+++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/WebhookRegistrationTrait.java
@@ -0,0 +1,134 @@
+/*
+ * 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.SCMSource;
+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 Class extends SCMSourceContext> getContextClass() {
+ return BitbucketSCMSourceContext.class;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Class extends SCMSource> getSourceClass() {
+ return BitbucketSCMSource.class;
+ }
+
+ /**
+ * 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/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 3d83d0b5e..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
@@ -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;
@@ -44,45 +46,42 @@
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.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 {
@@ -271,8 +270,19 @@ public void deletePullRequestApproval(String pullRequestId) throws IOException,
* {@inheritDoc}
*/
@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);
+ 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(branchOrHash));
+ for (String segment : StringUtils.split(path, "/")) {
+ url.append('/');
+ url.append(Util.rawEncode(segment));
+ }
+ int status = getRequestStatus(url.toString());
return status == HttpStatus.SC_OK;
}
@@ -333,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);
}
@@ -360,7 +373,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 +410,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 +424,14 @@ 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 +445,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 +504,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 +584,7 @@ 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 = getResponseContent(httpget, httpget.getResponseContentLength());
if (httpget.getStatusCode() == HttpStatus.SC_NOT_FOUND) {
throw new FileNotFoundException("URL: " + path);
}
@@ -631,7 +652,7 @@ private String postRequest(PostMethod httppost) throws IOException {
// 204, no content
return "";
}
- String response = new String(httppost.getResponseBody(), "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);
}
@@ -650,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);
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/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;
+ }
+
+}
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..1126cd587
--- /dev/null
+++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint.java
@@ -0,0 +1,139 @@
+/*
+ * 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();
+
+ /**
+ * 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.
+ *
+ * @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..40d2a9182
--- /dev/null
+++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpoint.java
@@ -0,0 +1,102 @@
+/*
+ * 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 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";
+ /**
+ * A bad URL of Bitbucket Cloud.
+ */
+ public static final String BAD_SERVER_URL = "http://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;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @NonNull
+ @Override
+ public String getRepositoryUrl(@NonNull String repoOwner, @NonNull String repository) {
+ return SERVER_URL + "/" + Util.rawEncode(repoOwner) + "/" + Util.rawEncode(repository);
+ }
+
+ /**
+ * 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..a520b7b6a
--- /dev/null
+++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketEndpointConfiguration.java
@@ -0,0 +1,308 @@
+/*
+ * 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.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;
+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)
+ || BitbucketCloudEndpoint.BAD_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 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 extends AbstractBitbucketEndpoint> endpoints) {
+ Jenkins.getActiveInstance().checkPermission(Jenkins.ADMINISTER);
+ List eps = new ArrayList<>(Util.fixNull(endpoints));
+ // remove duplicates and empty urls
+ Set serverUrls = new HashSet();
+ for (ListIterator iterator = eps.listIterator(); iterator.hasNext(); ) {
+ AbstractBitbucketEndpoint endpoint = iterator.next();
+ String serverUrl = endpoint.getServerUrl();
+ if (StringUtils.isBlank(serverUrl) || serverUrls.contains(serverUrl)) {
+ iterator.remove();
+ continue;
+ } 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);
+ }
+ if (eps.isEmpty()) {
+ eps.add(new BitbucketCloudEndpoint(false, null));
+ }
+ this.endpoints = eps;
+ 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) {
+ 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
new file mode 100644
index 000000000..8dff3dcc7
--- /dev/null
+++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketServerEndpoint.java
@@ -0,0 +1,130 @@
+/*
+ * 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.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 = BitbucketEndpointConfiguration.normalizeServerUrl(serverUrl);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @NonNull
+ @Override
+ 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.
+ */
+ @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/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PullRequestHookProcessor.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/PullRequestHookProcessor.java
index 0fe8cb10c..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,33 +25,40 @@
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;
import hudson.scm.SCM;
-import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
+import java.util.Set;
import 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 org.codehaus.jackson.map.ObjectMapper;
+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;
@@ -91,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;
@@ -145,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())) {
@@ -161,30 +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()
- );
- 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 extends BitbucketWebHook> 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 extends BitbucketWebHook> 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/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..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
@@ -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;
@@ -327,8 +318,20 @@ public void postBuildStatus(@NonNull BitbucketBuildStatus status) throws IOExcep
* {@inheritDoc}
*/
@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));
+ 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, "/")) {
+ 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(branchOrHash, "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);
}
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
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..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
@@ -1,30 +1,25 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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/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:
-
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.
+
- 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:
-
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)
+
- 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".
+
- 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.
+
- 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.
+ 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 @@
+
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..56edf6431 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,33 @@ 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.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
+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)
+
+ 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.
+
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.
+
+ 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.
+