Java tutorial
/* * The MIT License * * Copyright (c) 2016-2017, CloudBees, Inc., Nikolas Falco * * 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.BitbucketApiFactory; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBranch; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketCommit; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketHref; import com.cloudbees.jenkins.plugins.bitbucket.api.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.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.CredentialsNameProvider; import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.damnhandy.uri.template.UriTemplate; import com.fasterxml.jackson.databind.util.StdDateFormat; 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.TaskListener; import hudson.plugins.git.GitSCM; import hudson.plugins.mercurial.MercurialSCM; import hudson.plugins.mercurial.traits.MercurialBrowserSCMSourceTrait; import hudson.scm.SCM; import hudson.util.FormFillFailure; 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.Date; 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.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.authentication.tokens.api.AuthenticationTokens; 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; import jenkins.scm.api.SCMSourceCriteria.Probe; import jenkins.scm.api.SCMSourceDescriptor; import jenkins.scm.api.SCMSourceEvent; import jenkins.scm.api.SCMSourceOwner; 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.SCMSourceRequest.IntermediateLambda; import jenkins.scm.api.trait.SCMSourceTrait; import jenkins.scm.api.trait.SCMSourceTraitDescriptor; import jenkins.scm.impl.ChangeRequestSCMHeadCategory; import jenkins.scm.impl.TagSCMHeadCategory; 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.NoExternalUse; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; /** * SCM source implementation for Bitbucket. * * It provides a way to discover/retrieve branches and pull requests through the Bitbucket REST API * which is much faster than the plain Git SCM source implementation. */ public class BitbucketSCMSource extends SCMSource { private static final Logger LOGGER = Logger.getLogger(BitbucketSCMSource.class.getName()); private static final String CLOUD_REPO_TEMPLATE = "{/owner,repo}"; private static final String SERVER_REPO_TEMPLATE = "/projects{/owner}/repos{/repo}"; /** * Bitbucket URL. */ @NonNull private String serverUrl = BitbucketCloudEndpoint.SERVER_URL; /** * Credentials used to access the Bitbucket REST API. */ @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<SCMSourceTrait> 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. */ @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. */ @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. */ @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. * This value (or serverUrl if this is null) is used in particular * to find the current endpoint configuration for this server. */ @Deprecated @Restricted(NoExternalUse.class) @RestrictedSince("2.2.0") private transient String bitbucketServerUrl; /** * The cache of the repository type. */ @CheckForNull private transient BitbucketRepositoryType repositoryType; /** * The cache of pull request titles for each open PR. */ @CheckForNull private transient /*effectively final*/ Map<String, String> pullRequestTitleCache; /** * The cache of pull request contributors for each open PR. */ @CheckForNull private transient /*effectively final*/ Map<String, ContributorMetadataAction> pullRequestContributorCache; /** * The cache of the clone links. */ @CheckForNull private transient List<BitbucketHref> cloneLinks = null; /** * Constructor. * * @param repoOwner the repository owner. * @param repository the repository name. * @since 2.2.0 */ @DataBoundConstructor 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", "deprecation" }) @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 (serverUrl == null) { LOGGER.log(Level.WARNING, "BitbucketSCMSource::readResolve : serverUrl is still empty"); } if (traits == null) { traits = new ArrayList<>(); if (!"*".equals(includes) || !"".equals(excludes)) { traits.add(new WildcardSCMHeadFilterTrait(includes, excludes)); } if (checkoutCredentialsId != null && !DescriptorImpl.SAME.equals(checkoutCredentialsId) && !checkoutCredentialsId.equals(credentialsId)) { 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 public String getCredentialsId() { return credentialsId; } @DataBoundSetter public void setCredentialsId(@CheckForNull String credentialsId) { this.credentialsId = Util.fixEmpty(credentialsId); } @NonNull public String getRepoOwner() { return repoOwner; } @NonNull public String getRepository() { return repository; } @NonNull public String getServerUrl() { return serverUrl; } @DataBoundSetter public void setServerUrl(@CheckForNull String serverUrl) { this.serverUrl = BitbucketEndpointConfiguration.normalizeServerUrl(serverUrl); } @NonNull public String getEndpointJenkinsRootUrl() { return AbstractBitbucketEndpoint.getEndpointJenkinsRootUrl(serverUrl); } @NonNull public List<SCMSourceTrait> getTraits() { return Collections.unmodifiableList(traits); } @DataBoundSetter public void setTraits(@CheckForNull List<SCMSourceTrait> traits) { this.traits = new ArrayList<>(Util.fixNull(traits)); } @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); } @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 setCheckoutCredentialsId(String checkoutCredentialsId) { traits.removeIf(trait -> trait instanceof SSHCheckoutTrait); if (checkoutCredentialsId != null && !DescriptorImpl.SAME.equals(checkoutCredentialsId)) { traits.add(new SSHCheckoutTrait(checkoutCredentialsId)); } } @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 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, "")); } } @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 ""; } @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)); } } @Deprecated @Restricted(NoExternalUse.class) @RestrictedSince("2.2.0") @DataBoundSetter public void setAutoRegisterHook(boolean autoRegisterHook) { traits.removeIf(trait -> trait instanceof WebhookRegistrationTrait); 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 { if (repositoryType == null) { BitbucketRepository r = buildBitbucketClient().getRepository(); repositoryType = BitbucketRepositoryType.fromString(r.getScm()); Map<String, List<BitbucketHref>> links = r.getLinks(); if (links != null && links.containsKey("clone")) { cloneLinks = links.get("clone"); } } return repositoryType; } public BitbucketApi buildBitbucketClient() { return buildBitbucketClient(repoOwner, repository); } public BitbucketApi buildBitbucketClient(PullRequestSCMHead head) { return buildBitbucketClient(head.getRepoOwner(), head.getRepository()); } public BitbucketApi buildBitbucketClient(String repoOwner, String repository) { return BitbucketApiFactory.newInstance(getServerUrl(), authenticator(), repoOwner, repository); } @Override public void afterSave() { try { getRepositoryType(); } catch (InterruptedException | IOException e) { LOGGER.log(Level.FINE, "Could not determine repository type of " + getRepoOwner() + "/" + getRepository() + " on " + getServerUrl() + " for " + getOwner(), e); } } @Override protected void retrieve(@CheckForNull SCMSourceCriteria criteria, @NonNull SCMHeadObserver observer, @CheckForNull SCMHeadEvent<?> event, @NonNull TaskListener listener) throws IOException, InterruptedException { try (BitbucketSCMSourceRequest request = new BitbucketSCMSourceContext(criteria, observer) .withTraits(traits).newRequest(this, listener)) { StandardCredentials 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<BitbucketPullRequest>() { @Override protected Iterable<BitbucketPullRequest> create() { try { return (Iterable<BitbucketPullRequest>) buildBitbucketClient().getPullRequests(); } catch (IOException | InterruptedException e) { throw new BitbucketSCMSource.WrappedException(e); } } }); } if (request.isFetchBranches()) { request.setBranches(new LazyIterable<BitbucketBranch>() { @Override protected Iterable<BitbucketBranch> create() { try { return (Iterable<BitbucketBranch>) buildBitbucketClient().getBranches(); } catch (IOException | InterruptedException e) { throw new BitbucketSCMSource.WrappedException(e); } } }); } if (request.isFetchTags()) { request.setTags(new LazyIterable<BitbucketBranch>() { @Override protected Iterable<BitbucketBranch> create() { try { return (Iterable<BitbucketBranch>) buildBitbucketClient().getTags(); } catch (IOException | InterruptedException e) { throw new BitbucketSCMSource.WrappedException(e); } } }); } // 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()) { // Search tags retrieveTags(request); } } catch (WrappedException e) { e.unwrap(); } } private void retrievePullRequests(final BitbucketSCMSourceRequest request) throws IOException, InterruptedException { final String fullName = repoOwner + "/" + repository; class Skip extends IOException { } final BitbucketApi originBitbucket = buildBitbucketClient(); if (request.isSkipPublicPRs() && !originBitbucket.isPrivate()) { request.listener().getLogger().printf("Skipping pull requests for %s (public repository)%n", fullName); return; } request.listener().getLogger().printf("Looking up %s for pull requests%n", fullName); final Set<String> livePRs = new HashSet<>(); int count = 0; Map<Boolean, Set<ChangeRequestCheckoutStrategy>> strategies = request.getPRStrategies(); for (final BitbucketPullRequest pull : request.getPullRequests()) { String originalBranchName = pull.getSource().getBranch().getName(); request.listener().getLogger().printf("Checking PR-%s from %s and branch %s%n", pull.getId(), pull.getSource().getRepository().getFullName(), originalBranchName); 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(), authenticator(), 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, pull.getAuthorEmail())); try { // We store resolved hashes here so to avoid resolving the commits multiple times for (final ChangeRequestCheckoutStrategy strategy : strategies.get(fork)) { String branchName = "PR-" + pull.getId(); if (strategies.get(fork).size() > 1) { branchName = "PR-" + pull.getId() + "-" + strategy.name().toLowerCase(Locale.ENGLISH); } PullRequestSCMHead head; if (originBitbucket instanceof BitbucketCloudApiClient) { head = new PullRequestSCMHead( // branchName, // pullRepoOwner, // pullRepository, // repositoryType, // originalBranchName, // pull, // originOf(pullRepoOwner, pullRepository), // strategy); } else { head = new PullRequestSCMHead( // branchName, // repoOwner, // repository, // repositoryType, // originalBranchName, // pull, // originOf(pullRepoOwner, pullRepository), // strategy); } if (request.process(head, // () -> { // use branch instead of commit to postpone closure initialisation return new BranchHeadCommit(pull.getSource().getBranch()); }, // new BitbucketProbeFactory<>(pullBitbucket, request), // new BitbucketRevisionFactory<BitbucketCommit>(pullBitbucket) { @NonNull @Override public SCMRevision create(@NonNull SCMHead head, @Nullable BitbucketCommit sourceCommit) throws IOException, InterruptedException { try { // use branch instead of commit to postpone closure initialisation BranchHeadCommit targetCommit = new BranchHeadCommit( pull.getDestination().getBranch()); return super.create(head, sourceCommit, targetCommit); } 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 CriteriaWitness(request))) { request.listener().getLogger() // .format("%n %d pull requests were processed (query completed)%n", count); return; } } } catch (Skip e) { request.listener().getLogger().println("Do not have permission to view PR from " + pull.getSource().getRepository().getFullName() + " and branch " + originalBranchName); continue; } } request.listener().getLogger().format("%n %d pull requests were processed%n", count); getPullRequestTitleCache().keySet().retainAll(livePRs); getPullRequestContributorCache().keySet().retainAll(livePRs); } private void retrieveBranches(final BitbucketSCMSourceRequest request) throws IOException, InterruptedException { String fullName = repoOwner + "/" + repository; request.listener().getLogger().println("Looking up " + fullName + " for branches"); final BitbucketApi bitbucket = buildBitbucketClient(); Map<String, List<BitbucketHref>> links = bitbucket.getRepository().getLinks(); if (links != null && links.containsKey("clone")) { cloneLinks = links.get("clone"); } int count = 0; for (final BitbucketBranch branch : request.getBranches()) { request.listener().getLogger().println("Checking branch " + branch.getName() + " from " + fullName); count++; if (request.process( // new BranchSCMHead(branch.getName(), repositoryType), // (IntermediateLambda<BitbucketCommit>) () -> new BranchHeadCommit(branch), // new BitbucketProbeFactory<>(bitbucket, request), // new BitbucketRevisionFactory<>(bitbucket), // 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 retrieveTags(final BitbucketSCMSourceRequest request) throws IOException, InterruptedException { String fullName = repoOwner + "/" + repository; request.listener().getLogger().println("Looking up " + fullName + " for tags"); final BitbucketApi bitbucket = buildBitbucketClient(); Map<String, List<BitbucketHref>> links = bitbucket.getRepository().getLinks(); if (links != null && links.containsKey("clone")) { cloneLinks = links.get("clone"); } int count = 0; for (final BitbucketBranch tag : request.getTags()) { request.listener().getLogger().println("Checking tag " + tag.getName() + " from " + fullName); count++; if (request.process(new BitbucketTagSCMHead(tag.getName(), tag.getDateMillis(), repositoryType), // tag::getRawNode, // new BitbucketProbeFactory<>(bitbucket, request), // new BitbucketRevisionFactory<>(bitbucket), // new CriteriaWitness(request))) { request.listener().getLogger().format("%n %d tags were processed (query completed)%n", count); return; } } request.listener().getLogger().format("%n %d tags were processed%n", count); } @Override protected SCMRevision retrieve(SCMHead head, TaskListener listener) throws IOException, InterruptedException { final BitbucketApi bitbucket = buildBitbucketClient(); List<? extends BitbucketBranch> branches = bitbucket.getBranches(); if (head instanceof PullRequestSCMHead) { PullRequestSCMHead h = (PullRequestSCMHead) head; BitbucketCommit targetRevision = findCommit(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; } BitbucketCommit sourceRevision; if (bitbucket instanceof BitbucketCloudApiClient) { branches = head.getOrigin() == SCMHeadOrigin.DEFAULT ? branches : buildBitbucketClient(h).getBranches(); sourceRevision = findCommit(h.getBranchName(), branches, listener); } else { final List<? extends BitbucketPullRequest> pullRequests = bitbucket.getPullRequests(); sourceRevision = findPRCommit(h.getId(), pullRequests, listener); } if (sourceRevision == null) { LOGGER.log(Level.WARNING, "No revision found in {0}/{1} for PR-{2} [{3}]", new Object[] { h.getRepoOwner(), h.getRepository(), h.getId(), h.getBranchName() }); return null; } if (getRepositoryType() == BitbucketRepositoryType.MERCURIAL) { return new PullRequestSCMRevision<>(h, new MercurialRevision(h.getTarget(), targetRevision), new MercurialRevision(h, sourceRevision)); } else { return new PullRequestSCMRevision<>(h, new BitbucketGitSCMRevision(h.getTarget(), targetRevision), new BitbucketGitSCMRevision(h, sourceRevision)); } } else if (head instanceof BitbucketTagSCMHead) { BitbucketTagSCMHead tagHead = (BitbucketTagSCMHead) head; List<? extends BitbucketBranch> tags = bitbucket.getTags(); BitbucketCommit revision = findCommit(head.getName(), tags, listener); if (revision == null) { LOGGER.log(Level.WARNING, "No tag 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 BitbucketTagSCMRevision(tagHead, revision); } } else { BitbucketCommit revision = findCommit(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 BitbucketGitSCMRevision(head, revision); } } } private BitbucketCommit findCommit(String branchName, List<? extends BitbucketBranch> branches, TaskListener listener) { for (final BitbucketBranch b : branches) { if (branchName.equals(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%n", branchName); } return null; } return new BranchHeadCommit(b); } } listener.getLogger().format("Cannot find the branch %s%n", branchName); return null; } private BitbucketCommit findPRCommit(String prId, List<? extends BitbucketPullRequest> pullRequests, TaskListener listener) { for (BitbucketPullRequest pr : pullRequests) { if (prId.equals(pr.getId())) { // if I use getCommit() the branch closure is trigger immediately BitbucketBranch branch = pr.getSource().getBranch(); String hash = branch.getRawNode(); if (hash == null) { if (BitbucketCloudEndpoint.SERVER_URL.equals(getServerUrl())) { listener.getLogger().format("Cannot resolve the hash of the revision in PR-%s%n", prId); } else { listener.getLogger().format("Cannot resolve the hash of the revision in PR-%s. " + "Perhaps you are using Bitbucket Server previous to 4.x%n", prId); } return null; } return new BranchHeadCommit(branch); } } listener.getLogger().format("Cannot find the PR-%s%n", prId); return null; } @Override public SCM build(SCMHead head, SCMRevision revision) { BitbucketRepositoryType type; if (head instanceof PullRequestSCMHead) { type = ((PullRequestSCMHead) head).getRepositoryType(); } else if (head instanceof BranchSCMHead) { type = ((BranchSCMHead) head).getRepositoryType(); } else if (head instanceof BitbucketTagSCMHead) { type = ((BitbucketTagSCMHead) head).getRepositoryType(); } else { throw new IllegalArgumentException( "Either PullRequestSCMHead, BitbucketTagSCMHead or BranchSCMHead required as parameter"); } if (type == null) { if (revision instanceof MercurialRevision) { type = BitbucketRepositoryType.MERCURIAL; } else if (revision instanceof SCMRevisionImpl) { type = BitbucketRepositoryType.GIT; } else { try { type = getRepositoryType(); } catch (IOException | InterruptedException e) { type = BitbucketRepositoryType.GIT; LOGGER.log(Level.SEVERE, "Could not determine repository type of " + getRepoOwner() + "/" + getRepository() + " on " + getServerUrl() + " for " + getOwner() + " assuming " + type, e); } } } assert type != null; if (cloneLinks == null) { BitbucketApi bitbucket = buildBitbucketClient(); try { BitbucketRepository r = bitbucket.getRepository(); Map<String, List<BitbucketHref>> links = r.getLinks(); if (links != null && links.containsKey("clone")) { cloneLinks = links.get("clone"); } } catch (IOException | InterruptedException e) { LOGGER.log(Level.SEVERE, "Could not determine clone links of " + getRepoOwner() + "/" + getRepository() + " on " + getServerUrl() + " for " + getOwner() + " falling back to generated links", e); cloneLinks = new ArrayList<>(); cloneLinks.add(new BitbucketHref("ssh", bitbucket.getRepositoryUri(type, BitbucketRepositoryProtocol.SSH, null, getRepoOwner(), getRepository()))); cloneLinks.add(new BitbucketHref("https", bitbucket.getRepositoryUri(type, BitbucketRepositoryProtocol.HTTP, null, getRepoOwner(), getRepository()))); } } 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(); } } @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 public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } @CheckForNull /* package */ StandardCredentials credentials() { return BitbucketCredentials.lookupCredentials(getServerUrl(), getOwner(), getCredentialsId(), StandardCredentials.class); } @CheckForNull BitbucketAuthenticator authenticator() { return AuthenticationTokens.convert(BitbucketAuthenticator.authenticationContext(getServerUrl()), credentials()); } @NonNull @Override protected List<Action> retrieveActions(@CheckForNull SCMSourceEvent event, @NonNull TaskListener listener) throws IOException, InterruptedException { // TODO when we have support for trusted events, use the details from event if event was from trusted source List<Action> result = new ArrayList<>(); final BitbucketApi bitbucket = buildBitbucketClient(); BitbucketRepository r = bitbucket.getRepository(); Map<String, List<BitbucketHref>> links = r.getLinks(); if (links != null && links.containsKey("clone")) { cloneLinks = links.get("clone"); } result.add(new BitbucketRepoMetadataAction(r)); String defaultBranch = bitbucket.getDefaultBranch(); if (StringUtils.isNotBlank(defaultBranch)) { result.add(new BitbucketDefaultBranch(repoOwner, repository, defaultBranch)); } UriTemplate template; if (BitbucketCloudEndpoint.SERVER_URL.equals(getServerUrl())) { template = UriTemplate.fromTemplate(getServerUrl() + CLOUD_REPO_TEMPLATE); } else { template = UriTemplate.fromTemplate(getServerUrl() + SERVER_REPO_TEMPLATE); } template.set("owner", repoOwner).set("repo", repository); String url = template.expand(); result.add(new BitbucketLink("icon-bitbucket-repo", url)); result.add(new ObjectMetadataAction(r.getRepositoryName(), null, url)); return result; } @NonNull @Override protected List<Action> retrieveActions(@NonNull SCMHead head, @CheckForNull SCMHeadEvent event, @NonNull TaskListener listener) throws IOException, InterruptedException { // TODO when we have support for trusted events, use the details from event if event was from trusted source List<Action> result = new ArrayList<>(); UriTemplate template; String title = null; if (BitbucketCloudEndpoint.SERVER_URL.equals(getServerUrl())) { template = UriTemplate.fromTemplate(getServerUrl() + CLOUD_REPO_TEMPLATE + "/{branchOrPR}/{prIdOrHead}") .set("owner", repoOwner).set("repo", repository); if (head instanceof PullRequestSCMHead) { PullRequestSCMHead pr = (PullRequestSCMHead) head; template.set("branchOrPR", "pull-requests").set("prIdOrHead", pr.getId()); } else { template.set("branchOrPR", "branch").set("prIdOrHead", head.getName()); } } else { if (head instanceof PullRequestSCMHead) { PullRequestSCMHead pr = (PullRequestSCMHead) head; template = UriTemplate .fromTemplate(getServerUrl() + SERVER_REPO_TEMPLATE + "/pull-requests/{id}/overview") .set("owner", repoOwner).set("repo", repository).set("id", pr.getId()); } else { template = UriTemplate .fromTemplate(getServerUrl() + SERVER_REPO_TEMPLATE + "/compare/commits{?sourceBranch}") .set("owner", repoOwner).set("repo", repository) .set("sourceBranch", Constants.R_HEADS + head.getName()); } } if (head instanceof PullRequestSCMHead) { PullRequestSCMHead pr = (PullRequestSCMHead) head; title = getPullRequestTitleCache().get(pr.getId()); ContributorMetadataAction contributor = getPullRequestContributorCache().get(pr.getId()); if (contributor != null) { result.add(contributor); } } String url = template.expand(); result.add(new BitbucketLink("icon-bitbucket-branch", url)); result.add(new ObjectMetadataAction(title, null, url)); SCMSourceOwner owner = getOwner(); if (owner instanceof Actionable) { for (BitbucketDefaultBranch p : ((Actionable) owner).getActions(BitbucketDefaultBranch.class)) { if (StringUtils.equals(getRepoOwner(), p.getRepoOwner()) && StringUtils.equals(repository, p.getRepository()) && StringUtils.equals(p.getDefaultBranch(), head.getName())) { result.add(new PrimaryInstanceMetadataAction()); break; } } } return result; } @NonNull private synchronized Map<String, String> getPullRequestTitleCache() { if (pullRequestTitleCache == null) { pullRequestTitleCache = new ConcurrentHashMap<>(); } return pullRequestTitleCache; } @NonNull private synchronized Map<String, ContributorMetadataAction> getPullRequestContributorCache() { if (pullRequestContributorCache == null) { pullRequestContributorCache = new ConcurrentHashMap<>(); } 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 { public static final String ANONYMOUS = "ANONYMOUS"; public static final String SAME = "SAME"; @Override public String getDisplayName() { return "Bitbucket"; } @SuppressWarnings("unused") // used By stapler public FormValidation doCheckCredentialsId(@CheckForNull @AncestorInPath SCMSourceOwner context, @QueryParameter String value, @QueryParameter String serverUrl) { return BitbucketCredentials.checkCredentialsId(context, value, serverUrl); } @SuppressWarnings("unused") // used By stapler public static FormValidation doCheckServerUrl(@QueryParameter String value) { if (BitbucketEndpointConfiguration.get().findEndpoint(value) == null) { return FormValidation.error("Unregistered Server: " + value); } return FormValidation.ok(); } @SuppressWarnings("unused") // used By stapler public boolean isServerUrlSelectable() { return BitbucketEndpointConfiguration.get().isEndpointSelectable(); } @SuppressWarnings("unused") // used By stapler public ListBoxModel doFillServerUrlItems() { return BitbucketEndpointConfiguration.get().getEndpointItems(); } @SuppressWarnings("unused") // used By stapler public ListBoxModel doFillCredentialsIdItems(@AncestorInPath SCMSourceOwner context, @QueryParameter String serverUrl) { return BitbucketCredentials.fillCredentialsIdItems(context, serverUrl); } @SuppressWarnings("unused") // used By stapler public ListBoxModel 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(); StandardCredentials credentials = BitbucketCredentials.lookupCredentials(serverUrl, context, credentialsId, StandardCredentials.class); BitbucketAuthenticator authenticator = AuthenticationTokens .convert(BitbucketAuthenticator.authenticationContext(serverUrl), credentials); try { BitbucketApi bitbucket = BitbucketApiFactory.newInstance(serverUrl, authenticator, repoOwner, null); BitbucketTeam team = bitbucket.getTeam(); List<? extends BitbucketRepository> repositories = bitbucket .getRepositories(team != null ? null : UserRoleInRepository.CONTRIBUTOR); if (repositories.isEmpty()) { throw FormFillFailure.error(Messages.BitbucketSCMSource_NoMatchingOwner(repoOwner)) .withSelectionCleared(); } for (BitbucketRepository repo : repositories) { result.add(repo.getRepositoryName()); } return result; } catch (FormFillFailure | OutOfMemoryError e) { throw e; } catch (IOException e) { if (e instanceof BitbucketRequestException) { if (((BitbucketRequestException) e).getHttpCode() == 401) { throw FormFillFailure .error(credentials == null ? Messages.BitbucketSCMSource_UnauthorizedAnonymous(repoOwner) : Messages.BitbucketSCMSource_UnauthorizedOwner(repoOwner)) .withSelectionCleared(); } } else if (e.getCause() instanceof BitbucketRequestException) { if (((BitbucketRequestException) e.getCause()).getHttpCode() == 401) { throw FormFillFailure .error(credentials == null ? Messages.BitbucketSCMSource_UnauthorizedAnonymous(repoOwner) : Messages.BitbucketSCMSource_UnauthorizedOwner(repoOwner)) .withSelectionCleared(); } } LOGGER.log(Level.SEVERE, e.getMessage(), e); throw FormFillFailure.error(e.getMessage()); } catch (Throwable e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); throw FormFillFailure.error(e.getMessage()); } } @NonNull @Override protected SCMHeadCategory[] createCategories() { return new SCMHeadCategory[] { new UncategorizedSCMHeadCategory( Messages._BitbucketSCMSource_UncategorizedSCMHeadCategory_DisplayName()), new ChangeRequestSCMHeadCategory( Messages._BitbucketSCMSource_ChangeRequestSCMHeadCategory_DisplayName()), new TagSCMHeadCategory(Messages._BitbucketSCMSource_TagSCMHeadCategory_DisplayName()) // TODO add support for feature branch identification }; } public List<NamedArrayList<? extends SCMSourceTraitDescriptor>> getTraitsDescriptorLists() { List<SCMSourceTraitDescriptor> 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<SCMSourceTraitDescriptor> dedup = new HashSet<>(); for (Iterator<SCMSourceTraitDescriptor> 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<NamedArrayList<? extends SCMSourceTraitDescriptor>> 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", it -> GitSCM.class.isAssignableFrom(it.getScmClass()), true, result); NamedArrayList.select(all, "Mercurial", it -> MercurialSCM.class.isAssignableFrom(it.getScmClass()), true, result); NamedArrayList.select(all, "General", null, true, result, insertionPoint); return result; } public List<SCMSourceTrait> 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 { private static final long serialVersionUID = 1L; private final String hash; private final String author; private final String message; private final Date date; /** * Construct a Mercurial revision. * * @param head the {@link SCMHead} that represent this revision * @param hash of the head commit for the given head * @deprecated Use {@link #MercurialRevision(SCMHead, BitbucketCommit)} * instead of this */ @Deprecated public MercurialRevision(@NonNull final SCMHead head, @Nullable final String hash) { super(head); this.hash = hash; this.author = null; this.message = null; this.date = null; } /** * Construct a Mercurial revision. * * @param head the {@link SCMHead} that represent this revision * @param commit head */ public MercurialRevision(@NonNull final SCMHead head, @NonNull final BitbucketCommit commit) { super(head); this.hash = commit.getHash(); this.author = commit.getAuthor(); this.message = commit.getMessage(); this.date = new Date(commit.getDateMillis()); } /** * Returns the author of this revision in GIT format. * * @return commit author in the following format >name< >email< */ public String getAuthor() { return author; } /** * Returns the message associated with this revision. * * @return revision message */ public String getMessage() { return message; } /** * Return the revision date in ISO format. * * @return date for this revision */ public Date getDate() { return (Date) date.clone(); } public String getHash() { return hash; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } MercurialRevision that = (MercurialRevision) o; return Objects.equals(hash, that.hash) && Objects.equals(getHead(), that.getHead()); } @Override public int hashCode() { return Objects.hashCode(hash); } @Override public String toString() { return hash; } } 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<I> implements SCMSourceRequest.ProbeLambda<SCMHead, I> { private final BitbucketApi bitbucket; private final BitbucketSCMSourceRequest request; public BitbucketProbeFactory(BitbucketApi bitbucket, BitbucketSCMSourceRequest request) { this.bitbucket = bitbucket; this.request = request; } @NonNull @Override public Probe create(@NonNull final SCMHead head, @CheckForNull final I revisionInfo) throws IOException, InterruptedException { final String hash = (revisionInfo instanceof BitbucketCommit) // ? ((BitbucketCommit) revisionInfo).getHash() // : (String) revisionInfo; return new SCMSourceCriteria.Probe() { private static final long serialVersionUID = 1L; @Override public String name() { return head.getName(); } @Override public long lastModified() { try { BitbucketCommit commit = null; if (hash != null) { commit = (revisionInfo instanceof BitbucketCommit) // ? (BitbucketCommit) revisionInfo // : 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 { if (hash == null) { request.listener().getLogger() // .format("Can not resolve path for hash [%s] on repository %s/%s%n", // hash, bitbucket.getOwner(), bitbucket.getRepositoryName()); return false; } try { return bitbucket.checkPathExists(hash, path); } catch (InterruptedException e) { throw new IOException("Interrupted", e); } } }; } } private class BitbucketRevisionFactory<I> implements SCMSourceRequest.LazyRevisionLambda<SCMHead, SCMRevision, I> { private final BitbucketApi client; public BitbucketRevisionFactory(BitbucketApi client) { this.client = client; } @NonNull @Override public SCMRevision create(@NonNull SCMHead head, @Nullable I input) throws IOException, InterruptedException { return create(head, input, null); } @NonNull public SCMRevision create(@NonNull SCMHead head, @Nullable I sourceInput, @Nullable I targetInput) throws IOException, InterruptedException { BitbucketCommit sourceCommit = asCommit(sourceInput); BitbucketCommit targetCommit = asCommit(targetInput); SCMRevision revision; if (head instanceof PullRequestSCMHead) { PullRequestSCMHead prHead = (PullRequestSCMHead) head; SCMHead targetHead = prHead.getTarget(); if (repositoryType == BitbucketRepositoryType.MERCURIAL) { revision = new PullRequestSCMRevision<>( // prHead, // new MercurialRevision(targetHead, targetCommit), // new MercurialRevision(prHead, sourceCommit)); } else { return new PullRequestSCMRevision<>( // prHead, // new BitbucketGitSCMRevision(targetHead, targetCommit), // new BitbucketGitSCMRevision(prHead, sourceCommit)); } } else { if (repositoryType == BitbucketRepositoryType.MERCURIAL) { revision = new MercurialRevision(head, sourceCommit); } else { revision = new BitbucketGitSCMRevision(head, sourceCommit); } } return revision; } private BitbucketCommit asCommit(I input) throws IOException, InterruptedException { if (input instanceof String) { return client.resolveCommit((String) input); } else if (input instanceof BitbucketCommit) { return (BitbucketCommit) input; } return null; } } private static class BranchHeadCommit implements BitbucketCommit { private final BitbucketBranch branch; public BranchHeadCommit(@NonNull final BitbucketBranch branch) { this.branch = branch; } @Override public String getAuthor() { return branch.getAuthor(); } @Override public String getMessage() { return branch.getMessage(); } @Override public String getDate() { return new StdDateFormat().format(new Date(branch.getDateMillis())); } @Override public String getHash() { return branch.getRawNode(); } @Override public long getDateMillis() { return branch.getDateMillis(); } } private static class WrappedException extends RuntimeException { private static final long serialVersionUID = 1L; 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; } } }