Java tutorial
/* * The MIT License * * Copyright (c) 2016, CloudBees, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.cloudbees.jenkins.plugins.bitbucket; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApiFactory; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBranch; 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.sshcredentials.SSHUserPrivateKey; 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.StandardUsernamePasswordCredentials; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.Util; import hudson.model.Action; import hudson.model.Actionable; import hudson.model.TaskListener; import hudson.plugins.git.BranchSpec; 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.extensions.impl.SubmoduleOption; 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.scm.SCM; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.LinkedList; 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.scm.api.SCMHead; import jenkins.scm.api.SCMHeadCategory; import jenkins.scm.api.SCMHeadEvent; import jenkins.scm.api.SCMHeadObserver; import jenkins.scm.api.SCMRevision; import jenkins.scm.api.SCMSource; import jenkins.scm.api.SCMSourceCriteria; 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.impl.ChangeRequestSCMHeadCategory; import jenkins.scm.impl.UncategorizedSCMHeadCategory; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.WordUtils; import org.eclipse.jgit.lib.Constants; 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 Bitbuclet REST API * which is much faster than the plain Git SCM source implementation. */ public class BitbucketSCMSource extends SCMSource { /** * Credentials used to access the Bitbucket REST API. */ private String credentialsId; /** * Credentials used to clone the repository/repositories. */ private String checkoutCredentialsId; /** * Repository owner. * Used to build the repository URL. */ private final String repoOwner; /** * Repository name. * Used to build the repository URL. */ private final String repository; /** * Ant match expression that indicates what branches to include in the retrieve process. */ private String includes = "*"; /** * Ant match expression that indicates what branches to exclude in the retrieve process. */ private String excludes = ""; /** * If true, a webhook will be auto-registered in the repository managed by this source. */ private boolean autoRegisterHook = false; /** * Bitbucket Server URL. * An specific HTTP client is used if this field is not null. */ private String bitbucketServerUrl; /** * Port used by Bitbucket Server for SSH clone. * -1 by default (for Bitbucket Cloud). */ private int sshPort = -1; /** * Repository type. */ private 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; private static final Logger LOGGER = Logger.getLogger(BitbucketSCMSource.class.getName()); @DataBoundConstructor public BitbucketSCMSource(String id, String repoOwner, String repository) { super(id); this.repoOwner = repoOwner; this.repository = repository; } @CheckForNull public String getCredentialsId() { return credentialsId; } @DataBoundSetter public void setCredentialsId(String credentialsId) { this.credentialsId = Util.fixEmpty(credentialsId); } @CheckForNull public String getCheckoutCredentialsId() { return checkoutCredentialsId; } @DataBoundSetter public void setCheckoutCredentialsId(String checkoutCredentialsId) { this.checkoutCredentialsId = checkoutCredentialsId; } public String getIncludes() { return includes; } @DataBoundSetter public void setIncludes(@NonNull String includes) { Pattern.compile(getPattern(includes)); this.includes = includes; } public String getExcludes() { return excludes; } @DataBoundSetter public void setExcludes(@NonNull String excludes) { Pattern.compile(getPattern(excludes)); this.excludes = excludes; } public String getRepoOwner() { return repoOwner; } public String getRepository() { return repository; } @DataBoundSetter public void setAutoRegisterHook(boolean autoRegisterHook) { this.autoRegisterHook = autoRegisterHook; } public boolean isAutoRegisterHook() { return autoRegisterHook; } public int getSshPort() { return sshPort; } @DataBoundSetter public void setSshPort(int sshPort) { this.sshPort = sshPort; } @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("/$", ""); } } @CheckForNull public String getBitbucketServerUrl() { return bitbucketServerUrl; } private String bitbucketUrl() { return StringUtils.defaultIfBlank(bitbucketServerUrl, "https://bitbucket.org"); } 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 (sshPort > 0) { protocolPortOverride = sshPort; } } else { protocol = BitbucketRepositoryProtocol.HTTP; } return buildBitbucketClient().getRepositoryUri(repositoryType, protocol, protocolPortOverride, repoOwner, repository); } public BitbucketRepositoryType getRepositoryType() throws IOException, InterruptedException { if (repositoryType == null) { repositoryType = BitbucketRepositoryType.fromString(buildBitbucketClient().getRepository().getScm()); } return repositoryType; } public BitbucketApi buildBitbucketClient() { return BitbucketApiFactory.newInstance(bitbucketServerUrl, getScanCredentials(), repoOwner, repository); } public BitbucketApi buildBitbucketClient(PullRequestSCMHead head) { return BitbucketApiFactory.newInstance(bitbucketServerUrl, getScanCredentials(), head.getRepoOwner(), head.getRepository()); } @Override public void afterSave() { try { getRepositoryType(); } 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); } } @Override protected void retrieve(@CheckForNull SCMSourceCriteria criteria, @NonNull SCMHeadObserver observer, @CheckForNull SCMHeadEvent<?> event, @NonNull TaskListener listener) throws IOException, InterruptedException { 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())); // Search branches retrieveBranches(criteria, observer, listener); // Search pull requests retrievePullRequests(criteria, observer, listener); } private void retrievePullRequests(SCMSourceCriteria criteria, SCMHeadObserver observer, final TaskListener listener) 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<String> livePRs = new HashSet<>(); for (final BitbucketPullRequest pull : pulls) { checkInterrupt(); listener.getLogger().println("Checking PR from " + pull.getSource().getRepository().getFullName() + " and branch " + pull.getSource().getBranch().getName()); // Resolve full hash. See https://bitbucket.org/site/master/issues/11415/pull-request-api-should-return-full-commit 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; } else { // this is some other unexpected error, we need to abort observing, so throw. throw e; } } 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, pull); if (!observer.isObserving()) { return; } } getPullRequestTitleCache().keySet().retainAll(livePRs); getPullRequestContributorCache().keySet().retainAll(livePRs); } else { listener.getLogger().format("Skipping pull requests for public repositories%n"); } } private void retrieveBranches(SCMSourceCriteria criteria, @NonNull final SCMHeadObserver observer, @NonNull TaskListener listener) throws IOException, InterruptedException { String fullName = repoOwner + "/" + repository; listener.getLogger().println("Looking up " + fullName + " for branches"); final BitbucketApi bitbucket = buildBitbucketClient(); List<? extends BitbucketBranch> branches = bitbucket.getBranches(); for (BitbucketBranch branch : branches) { checkInterrupt(); listener.getLogger().println("Checking branch " + branch.getName() + " from " + fullName); observe(criteria, observer, listener, repoOwner, repository, branch.getName(), branch.getRawNode(), null); } } private void observe(SCMSourceCriteria criteria, SCMHeadObserver observer, final TaskListener listener, final String owner, final String repositoryName, final String branchName, final String hash, BitbucketPullRequest pr) throws IOException, InterruptedException { if (isExcluded(branchName)) { return; } final BitbucketApi bitbucket = BitbucketApiFactory.newInstance(bitbucketServerUrl, getScanCredentials(), owner, repositoryName); SCMSourceCriteria branchCriteria = criteria; if (branchCriteria != 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 (branchCriteria.isHead(probe, listener)) { listener.getLogger().println("Met criteria"); } else { listener.getLogger().println("Does not meet criteria"); return; } } BitbucketRepositoryType repositoryType = getRepositoryType(); SCMRevision revision; SCMHead head = pr != null ? new PullRequestSCMHead(owner, repositoryName, repositoryType, branchName, pr) : new BranchSCMHead(branchName, repositoryType); if (repositoryType == BitbucketRepositoryType.MERCURIAL) { revision = new MercurialRevision(head, hash); } else { revision = new AbstractGitSCMSource.SCMRevisionImpl(head, hash); } 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(); 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()); } 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()); } return null; } if (getRepositoryType() == BitbucketRepositoryType.MERCURIAL) { return new MercurialRevision(head, b.getRawNode()); } else { return new AbstractGitSCMSource.SCMRevisionImpl(head, b.getRawNode()); } } } 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() }); return null; } @Override public SCM build(SCMHead head, SCMRevision revision) { BitbucketRepositoryType repositoryType; if (head instanceof PullRequestSCMHead) { repositoryType = ((PullRequestSCMHead) head).getRepositoryType(); } else if (head instanceof BranchSCMHead) { repositoryType = ((BranchSCMHead) head).getRepositoryType(); } else { throw new IllegalArgumentException("Either PullRequestSCMHead or BranchSCMHead required as parameter"); } if (repositoryType == null) { if (revision instanceof MercurialRevision) { repositoryType = BitbucketRepositoryType.MERCURIAL; } else if (revision instanceof AbstractGitSCMSource.SCMRevisionImpl) { repositoryType = BitbucketRepositoryType.GIT; } else { try { repositoryType = getRepositoryType(); } catch (IOException | InterruptedException e) { repositoryType = 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); } } } 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 List<GitSCMExtension> extensions = new LinkedList<GitSCMExtension>(); BuildChooser buildChooser = revision instanceof AbstractGitSCMSource.SCMRevisionImpl ? new SpecificRevisionBuildChooser((AbstractGitSCMSource.SCMRevisionImpl) revision) : new DefaultBuildChooser(); extensions.add(new BuildChooserSetting(buildChooser)); extensions.add(new SubmoduleOption(false, true, false, "", 10000, true)); return new GitSCM(getGitRemoteConfigs(h), Collections.singletonList(new BranchSpec(h.getBranchName())), false, Collections.<SubmoduleConfig>emptyList(), null, null, extensions); } } // 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 List<GitSCMExtension> extensions = new LinkedList<GitSCMExtension>(); BuildChooser buildChooser = revision instanceof AbstractGitSCMSource.SCMRevisionImpl ? new SpecificRevisionBuildChooser((AbstractGitSCMSource.SCMRevisionImpl) revision) : new DefaultBuildChooser(); extensions.add(new BuildChooserSetting(buildChooser)); extensions.add(new SubmoduleOption(false, true, false, "", 10000, true)); return new GitSCM(getGitRemoteConfigs((BranchSCMHead) head), Collections.singletonList(new BranchSpec(head.getName())), false, Collections.<SubmoduleConfig>emptyList(), null, null, extensions); } } protected List<UserRemoteConfig> getGitRemoteConfigs(BranchSCMHead head) { List<UserRemoteConfig> result = new ArrayList<UserRemoteConfig>(); String remote = getRemote(repoOwner, repository, BitbucketRepositoryType.GIT); result.add(new UserRemoteConfig(remote, getRemoteName(), "+refs/heads/" + head.getName(), getCheckoutEffectiveCredentials())); return result; } protected List<UserRemoteConfig> getGitRemoteConfigs(PullRequestSCMHead head) { List<UserRemoteConfig> result = new ArrayList<UserRemoteConfig>(); String remote = getRemote(head.getRepoOwner(), head.getRepository(), BitbucketRepositoryType.GIT); result.add(new UserRemoteConfig(remote, getRemoteName(), "+refs/heads/" + head.getBranchName(), getCheckoutEffectiveCredentials())); return result; } @Override public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } @CheckForNull /* package */ StandardUsernamePasswordCredentials getScanCredentials() { return BitbucketCredentials.lookupCredentials(bitbucketServerUrl, getOwner(), credentialsId, 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<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(); result.add(new BitbucketRepoMetadataAction(r)); String defaultBranch = bitbucket.getDefaultBranch(); if (StringUtils.isNotBlank(defaultBranch)) { result.add(new BitbucketDefaultBranch(repoOwner, repository, defaultBranch)); } String serverUrl = StringUtils.removeEnd(bitbucketUrl(), "/"); if (StringUtils.isNotEmpty(bitbucketServerUrl)) { result.add(new BitbucketLink("icon-bitbucket-repo", serverUrl + "/projects/" + repoOwner + "/repos/" + repository)); result.add(new ObjectMetadataAction(r.getFullName(), null, serverUrl + "/projects/" + repoOwner + "/repos/" + repository)); } else { result.add(new BitbucketLink("icon-bitbucket-repo", serverUrl + "/" + repoOwner + "/" + repository)); result.add(new ObjectMetadataAction(r.getFullName(), null, serverUrl + "/" + repoOwner + "/" + repository)); } 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<>(); String serverUrl = StringUtils.removeEnd(bitbucketUrl(), "/"); if (StringUtils.isNotEmpty(bitbucketServerUrl)) { String branchUrl; String title; if (head instanceof PullRequestSCMHead) { PullRequestSCMHead pr = (PullRequestSCMHead) head; 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 = "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)); } else { String branchUrl; String title; if (head instanceof PullRequestSCMHead) { PullRequestSCMHead pr = (PullRequestSCMHead) head; 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 = repoOwner + "/" + repository + "/branch/" + head.getName(); title = null; } result.add(new BitbucketLink("icon-bitbucket-branch", serverUrl + "/" + branchUrl)); result.add(new ObjectMetadataAction(title, null, serverUrl + "/" + branchUrl)); } 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; } @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"; } public FormValidation doCheckCredentialsId(@QueryParameter String value, @QueryParameter String bitbucketServerUrl) { if (!value.isEmpty()) { return FormValidation.ok(); } else { return FormValidation.warning("Credentials are required for notifications"); } } public static FormValidation doCheckBitbucketServerUrl(@QueryParameter String bitbucketServerUrl) { String url = Util.fixEmpty(bitbucketServerUrl); if (url == null) { return FormValidation.ok(); } try { new URL(bitbucketServerUrl); } catch (MalformedURLException e) { return FormValidation.error("Invalid URL: " + e.getMessage()); } return FormValidation.ok(); } public ListBoxModel doFillCredentialsIdItems(@AncestorInPath SCMSourceOwner context, @QueryParameter String bitbucketServerUrl) { StandardListBoxModel result = new StandardListBoxModel(); result.includeEmptyValue(); return BitbucketCredentials.fillCredentials(bitbucketServerUrl, context, result); } 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); } @NonNull @Override protected SCMHeadCategory[] createCategories() { return new SCMHeadCategory[] { new UncategorizedSCMHeadCategory( Messages._BitbucketSCMSource_UncategorizedSCMHeadCategory_DisplayName()), new ChangeRequestSCMHeadCategory( Messages._BitbucketSCMSource_ChangeRequestSCMHeadCategory_DisplayName()) // TODO add support for tags and maybe feature branch identification }; } } public static class MercurialRevision extends SCMRevision { private static final long serialVersionUID = 1L; private String hash; public MercurialRevision(SCMHead head, String hash) { super(head); this.hash = hash; } 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 StringUtils.equals(hash, that.hash) && getHead().equals(that.getHead()); } @Override public int hashCode() { return hash != null ? hash.hashCode() : 0; } } }