From 5fa1952a2e582f2c428584c5ccc1800132559df0 Mon Sep 17 00:00:00 2001 From: Igor Pashev Date: Tue, 5 Dec 2017 11:37:56 +0300 Subject: Version 0.1.0 --- .clang-format | 22 ++ .gitignore | 4 - .travis.yml | 11 - ChangeLog.md | 5 + README.md | 133 +------ pom.xml | 48 +-- .../bitbucketpullrequestbuilder/.gitignore | 20 -- ...tAdditionalParameterEnvironmentContributor.java | 40 --- .../BitbucketBuildFilter.java | 241 ------------- .../BitbucketBuildListener.java | 58 --- .../BitbucketBuildTrigger.java | 332 ----------------- .../BitbucketBuilds.java | 54 --- .../BitbucketCause.java | 93 ----- .../BitbucketPullRequestsBuilder.java | 87 ----- .../BitbucketRepository.java | 346 ------------------ .../bitbucket/ApiClient.java | 285 --------------- .../bitbucket/BuildState.java | 10 - .../bitbucket/Pullrequest.java | 392 --------------------- ...tAdditionalParameterEnvironmentContributor.java | 39 ++ .../plugins/bbprb/BitbucketBuildListener.java | 69 ++++ .../plugins/bbprb/BitbucketBuildTrigger.java | 303 ++++++++++++++++ .../jenkinsci/plugins/bbprb/BitbucketCause.java | 80 +++++ .../plugins/bbprb/BitbucketHookReceiver.java | 140 ++++++++ .../plugins/bbprb/bitbucket/ApiClient.java | 246 +++++++++++++ .../plugins/bbprb/bitbucket/BuildState.java | 9 + .../BitbucketBuildTrigger/config.jelly | 48 --- .../BitbucketBuildTrigger/help-branchesFilter.html | 19 - .../help-branchesFilterBySCMIncludes.html | 7 - .../help-cancelOutdatedJobs.html | 1 - .../BitbucketBuildTrigger/help-ciKey.html | 4 - .../BitbucketBuildTrigger/help-ciName.html | 1 - .../BitbucketBuildTrigger/help-ciSkipPhrases.html | 5 - src/main/resources/index.jelly | 2 +- .../bbprb/BitbucketBuildTrigger/config.jelly | 21 ++ .../help-cancelOutdatedJobs.html | 1 + .../bbprb/BitbucketBuildTrigger/help-ciKey.html | 4 + .../bbprb/BitbucketBuildTrigger/help-ciName.html | 1 + src/test/java/BitbucketBuildFilterTest.java | 308 ---------------- src/test/java/BitbucketBuildRepositoryTest.java | 354 ------------------- 39 files changed, 961 insertions(+), 2882 deletions(-) create mode 100644 .clang-format delete mode 100644 .gitignore delete mode 100644 .travis.yml create mode 100644 ChangeLog.md delete mode 100644 src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/.gitignore delete mode 100644 src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketAdditionalParameterEnvironmentContributor.java delete mode 100644 src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildFilter.java delete mode 100644 src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildListener.java delete mode 100644 src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger.java delete mode 100644 src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuilds.java delete mode 100644 src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketCause.java delete mode 100644 src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketPullRequestsBuilder.java delete mode 100644 src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketRepository.java delete mode 100644 src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/ApiClient.java delete mode 100644 src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/BuildState.java delete mode 100644 src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/Pullrequest.java create mode 100644 src/main/java/org/jenkinsci/plugins/bbprb/BitbucketAdditionalParameterEnvironmentContributor.java create mode 100644 src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildListener.java create mode 100644 src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger.java create mode 100644 src/main/java/org/jenkinsci/plugins/bbprb/BitbucketCause.java create mode 100644 src/main/java/org/jenkinsci/plugins/bbprb/BitbucketHookReceiver.java create mode 100644 src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/ApiClient.java create mode 100644 src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/BuildState.java delete mode 100644 src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/config.jelly delete mode 100644 src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-branchesFilter.html delete mode 100644 src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-branchesFilterBySCMIncludes.html delete mode 100644 src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-cancelOutdatedJobs.html delete mode 100644 src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-ciKey.html delete mode 100644 src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-ciName.html delete mode 100644 src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-ciSkipPhrases.html create mode 100644 src/main/resources/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger/config.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger/help-cancelOutdatedJobs.html create mode 100644 src/main/resources/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger/help-ciKey.html create mode 100644 src/main/resources/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger/help-ciName.html delete mode 100644 src/test/java/BitbucketBuildFilterTest.java delete mode 100644 src/test/java/BitbucketBuildRepositoryTest.java diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..419e545 --- /dev/null +++ b/.clang-format @@ -0,0 +1,22 @@ +--- +Language: Java + +AccessModifierOffset: 0 +AlignAfterOpenBracket: true +AllowShortBlocksOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +BreakAfterJavaFieldAnnotations: true +BreakBeforeBinaryOperators: false +BreakBeforeBraces: Attach +ColumnLimit: 80 +IndentCaseLabels: true +IndentWidth: 2 +MaxEmptyLinesToKeep: 1 +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpacesInAngles: false +SpacesInParentheses: false +TabWidth: 2 +UseTab: Never diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 4ed3c09..0000000 --- a/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.idea -bitbucket-pullrequest-builder.iml -target/ -work diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6a2566e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -language: java - -jdk: - - openjdk7 - - oraclejdk7 - -script: - mvn install -U - -after_failure: - - cat target/surefire-reports/*.txt diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100644 index 0000000..ea3d117 --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,5 @@ +0.1.0 +===== + + * Can handle creating and updating pull requests. + diff --git a/README.md b/README.md index 88e74ea..74514e7 100644 --- a/README.md +++ b/README.md @@ -1,135 +1,16 @@ -Bitbucket Pull Request Builder Plugin -===================================== +BBPRB +===== -This Jenkins plugin builds pull requests from Bitbucket.org and will report the test results. +This plugin is a revision of original [Bitbucket Pull Request Builder Plugin](https://wiki.jenkins.io/display/JENKINS/Bitbucket+pullrequest+builder+plugin). +It was started by adding support for [Bitbucket webhooks](https://confluence.atlassian.com/bitbucket/manage-webhooks-735643732.html) +and resulted in massive rewrite and deleting the code. -[![Build Status](https://travis-ci.org/nishio-dens/bitbucket-pullrequest-builder-plugin.svg?branch=master)](https://travis-ci.org/nishio-dens/bitbucket-pullrequest-builder-plugin) - - -Prerequisites -------------- - -- Jenkins 1.625.3 or higher. -- https://wiki.jenkins-ci.org/display/JENKINS/Git+Plugin - - -Creating a Job -------------- - -- Create a new job -- Select and configure Git SCM - - Add Repository URL, `git@bitbucket.org:${repositoryOwner}/${repositoryName}.git` - - In Branch Specifier, type `*/${sourceBranch}` -- Under Build Triggers, check Bitbucket Pull Request Builder -- In Cron, enter crontab for this job. - - e.g. `* * * * *` will check for new pull requests every minute -- In Bitbucket BasicAuth Username, write your bitbucket username, like `jenkins@densan-labs.net` -- In Bitbucket BasicAuth Password, write your password -- In CI Identifier, enter an unique identifier among your Jenkins jobs related to the repo -- In CI Name, enter a human readable name for your Jenkins server -- Write RepositoryOwner -- Write RepositoryName -- Save to preserve your changes - - -Jenkins pipeline -------------- -``` -pipeline { - agent any - triggers{ - bitbucketpr(projectPath:'', - cron:'H/15 * * * *', - credentialsId:'', - username:'', - password:'', - repositoryOwner:'', - repositoryName:'', - branchesFilter:'', - branchesFilterBySCMIncludes:false, - ciKey:'', - ciName:'', - ciSkipPhrases:'', - checkDestinationCommit:false, - approveIfSuccess:false, - cancelOutdatedJobs:true, - commentTrigger:'') - } -} -``` - -After you set up your Jenkins pipeline, run the job for the first time manually (otherwise the trigger may not work!) - - -Merge the Pull Request's Source Branch into the Target Branch Before Building ------------------------------------------------------------------------------ - -You may want Jenkins to attempt to merge your PR before building. -This may help expose inconsistencies between the source branch and target branch. -Note that if the merge cannot be completed, the build will fail immediately. - -- Follow the steps above in "Creating a Job" -- In the "Source Code Management" > "Git" > "Additional Behaviors" section, click "Add" > "Merge Before Building" -- In "Name of Repository" put "origin" (or, if not using default name, use your remote repository's name. Note: unlike in the main part of the Git Repository config, you cannot leave this item blank for "default"). -- In "Branch to merge to" put "${targetBranch}" -- Note that as long as you don't push these changes to your remote repository, the merge only happens in your local repository. - -If you are merging into your target branch, you might want Jenkins to do a new build of the Pull Request when the target branch changes. -- There is a checkbox that says, "Rebuild if destination branch changes?" which enables this check. - - -Rerun a Build -------------- - -If you want to rerun a pull request build, write a comment on your pull request reading “test this please”. - - -Environment Variables Provided ------------------------------- - -- `sourceBranch` -- `targetBranch` -- `repositoryOwner` -- `repositoryName` -- `pullRequestId` -- `destinationRepositoryOwner` -- `destinationRepositoryName` -- `pullRequestTitle` -- `pullRequestAuthor` - - -Contributing to Bitbucket Pull Request Builder Plugin ------------------------------------------------------ - -- Do not use Fork [jenkinsci/bitbucket-pullrequest-builder-plugin](https://github.com/jenkinsci/bitbucket-pullrequest-builder-plugin) for contribution - -- Use project [nishio-dens/bitbucket-pullrequest-builder-plugin](https://github.com/nishio-dens/bitbucket-pullrequest-builder-plugin) - -- Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet. - -- Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it. - -- Fork the project. - -- Start a feature/bugfix branch. - -- Commit and push until you are happy with your contribution. - - - -Donations ------------------------------------------------------ -Do you like this plugin? feel free to donate! - -Paypal: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=LTXCF78GJ7224 - -BTC: 1KgwyVzefeNzJhuzqLq36E3bZi2WFjibMr - -Thank you! Copyright --------- +Copyright © 2017 Igor Pashev + Copyright © 2014 S.nishio + Martin Damovsky diff --git a/pom.xml b/pom.xml index f204feb..3239296 100644 --- a/pom.xml +++ b/pom.xml @@ -1,3 +1,4 @@ + 4.0.0 @@ -5,21 +6,23 @@ plugin 2.11 - - bitbucket-pullrequest-builder + bbprb Bitbucket Pullrequest Builder Plugin - 1.4.26-SNAPSHOT + 0.1.0 This Jenkins plugin builds pull requests from Bitbucket.org and will report the test results. hpi - https://wiki.jenkins-ci.org/display/JENKINS/Bitbucket+pullrequest+builder+plugin - + - scm:git:ssh://git@github.com/jenkinsci/bitbucket-pullrequest-builder-plugin.git - scm:git:ssh://git@github.com/jenkinsci/bitbucket-pullrequest-builder-plugin.git - https://github.com/jenkinsci/bitbucket-pullrequest-builder-plugin.git + https://github.com/ip1981/bbprb.git + https://github.com/ip1981/bbprb.git HEAD + + ip1981 + Igor Pashev + pashev.igor@gmail.com + nishio_dens nishio_dens @@ -30,32 +33,15 @@ Martin Damovsky martin.damovsky@gmail.com - - + - + repo.jenkins-ci.org http://repo.jenkins-ci.org/public/ - - - org.apache.maven.wagon - wagon-http - 2.4 - - - commons-httpclient - commons-httpclient - 3.1 - - - commons-codec - commons-codec - 1.9 - org.codehaus.jackson jackson-jaxrs @@ -76,19 +62,11 @@ guava 14.0-rc3 - - org.easymock - easymock - 3.4 - test - - repo.jenkins-ci.org http://repo.jenkins-ci.org/public/ - diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/.gitignore b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/.gitignore deleted file mode 100644 index 21e5893..0000000 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/.gitignore +++ /dev/null @@ -1,20 +0,0 @@ -target/ -work/ - -# -# Eclipse metadata. -# -.project -.classpath -.settings/ - -# -# Eclipse and Maven build results -# -bin/ - -# IntelliJ metadata. -*.iml -*.ipr -*.iws -.idea/ diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketAdditionalParameterEnvironmentContributor.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketAdditionalParameterEnvironmentContributor.java deleted file mode 100644 index c531ca1..0000000 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketAdditionalParameterEnvironmentContributor.java +++ /dev/null @@ -1,40 +0,0 @@ -package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; - -import hudson.EnvVars; -import hudson.Extension; -import hudson.model.*; - -import java.io.IOException; - -@Extension -public class BitbucketAdditionalParameterEnvironmentContributor extends EnvironmentContributor { - @Override - public void buildEnvironmentFor(Run run, EnvVars envVars, TaskListener taskListener) - throws IOException, InterruptedException { - - BitbucketCause cause = (BitbucketCause) run.getCause(BitbucketCause.class); - if (cause == null) { - return; - } - - putEnvVar(envVars, "sourceBranch", cause.getSourceBranch()); - putEnvVar(envVars, "targetBranch", cause.getTargetBranch()); - putEnvVar(envVars, "repositoryOwner", cause.getRepositoryOwner()); - putEnvVar(envVars, "repositoryName", cause.getRepositoryName()); - putEnvVar(envVars, "pullRequestId", cause.getPullRequestId()); - putEnvVar(envVars, "destinationRepositoryOwner", cause.getDestinationRepositoryOwner()); - putEnvVar(envVars, "destinationRepositoryName", cause.getDestinationRepositoryName()); - putEnvVar(envVars, "pullRequestTitle", cause.getPullRequestTitle()); - putEnvVar(envVars, "pullRequestAuthor", cause.getPullRequestAuthor()); - - } - - private static void putEnvVar(EnvVars envs, String name, String value) { - envs.put(name, getString(value, "")); - } - - private static String getString(String actual, String d) { - return actual == null ? d : actual; - } - -} diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildFilter.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildFilter.java deleted file mode 100644 index 6aa7344..0000000 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildFilter.java +++ /dev/null @@ -1,241 +0,0 @@ -package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; - -import java.util.logging.Logger; -import java.util.regex.Pattern; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.logging.Level; -import java.util.regex.Matcher; -import jenkins.plugins.git.AbstractGitSCMSource; -import jenkins.scm.api.SCMSource; - -/** - * Mutable wrapper - */ -class Mutable { - private T value; - public Mutable() { this.value = null; } - public Mutable(T value) { this.value = value; } - T get() { return this.value; } - void set(T value) { this.value = value; } -} - -abstract class Filter { - protected static final Logger logger = Logger.getLogger(BitbucketBuildTrigger.class.getName()); - - public static final String RX_FILTER_FLAG = "r"; - public static final String RX_FILTER_FLAG_SINGLE = RX_FILTER_FLAG + ":"; - - public static final String SRC_RX = "s:(" + RX_FILTER_FLAG_SINGLE + ")?"; - public static final String DST_RX = "d:(" + RX_FILTER_FLAG_SINGLE + ")?"; - public static final String AUTHOR_RX = "a:(" + RX_FILTER_FLAG_SINGLE + ")?"; - public static final String BRANCH_FILTER_RX_PART = "([^\\s$]*)"; - - abstract public boolean apply(String filter, BitbucketCause cause); - abstract public boolean check(String filter); - - static final Pattern RX_SRC_DST_PARTS = Pattern.compile("(s:)|(d:)"); - public static boolean HasSourceOrDestPartsPredicate(String filter) { return RX_SRC_DST_PARTS.matcher(filter).find(); } - - static final Pattern RX_AUTHOR_PARTS = Pattern.compile("(a:)"); - public static boolean HasAuthorPartsPredicate(String filter) { return RX_AUTHOR_PARTS.matcher(filter).find(); } - - protected boolean applyByRx(Pattern rx, Filter usedFilter, String filter, BitbucketCause cause) { - Matcher srcMatch = rx.matcher(filter); - boolean apply = false; - while (srcMatch.find()) { - String computedFilter = ((srcMatch.group(1) == null ? "" : srcMatch.group(1)) + srcMatch.group(2)).trim(); - logger.log(Level.FINE, "Apply computed filter: {0}", computedFilter); - apply = apply || (computedFilter.isEmpty() ? true : usedFilter.apply(computedFilter, cause)); - } - return apply; - } -} - -class EmptyFilter extends Filter { - @Override - public boolean apply(String filter, BitbucketCause cause) { return true; } - @Override - public boolean check(String filter) { return true; } -} - -class AnyFlag extends Filter { - @Override - public boolean apply(String filter, BitbucketCause cause) { return true; } - @Override - public boolean check(String filter) { return filter.isEmpty() || filter.contains("*") || filter.toLowerCase().contains("any"); } -} - -class OnlySourceFlag extends Filter { - @Override - public boolean apply(String filter, BitbucketCause cause) { - String selectedRx = filter.startsWith(RX_FILTER_FLAG_SINGLE) ? filter.substring(RX_FILTER_FLAG_SINGLE.length()) : Pattern.quote(filter); - logger.log(Level.FINE, "OnlySourceFlag using filter: {0}", selectedRx); - Matcher matcher = Pattern.compile(selectedRx, Pattern.CASE_INSENSITIVE).matcher(cause.getSourceBranch()); - return filter.startsWith(RX_FILTER_FLAG_SINGLE) ? matcher.find() : matcher.matches(); - } - @Override - public boolean check(String filter) { - return false; - } -} - -class OnlyDestFlag extends Filter { - @Override - public boolean apply(String filter, BitbucketCause cause) { - String selectedRx = filter.startsWith(RX_FILTER_FLAG_SINGLE) ? filter.substring(RX_FILTER_FLAG_SINGLE.length()) : Pattern.quote(filter); - logger.log(Level.FINE, "OnlyDestFlag using filter: {0}", selectedRx); - Matcher matcher = Pattern.compile(selectedRx, Pattern.CASE_INSENSITIVE).matcher(cause.getTargetBranch()); - return filter.startsWith(RX_FILTER_FLAG_SINGLE) ? matcher.find() : matcher.matches(); - } - @Override - public boolean check(String filter) { - return !HasSourceOrDestPartsPredicate(filter); - } -} - -class SourceDestFlag extends Filter { - static final Pattern SRC_MATCHER_RX = Pattern.compile(SRC_RX + BRANCH_FILTER_RX_PART, Pattern.CASE_INSENSITIVE | Pattern.CANON_EQ); - static final Pattern DST_MATCHER_RX = Pattern.compile(DST_RX + BRANCH_FILTER_RX_PART, Pattern.CASE_INSENSITIVE | Pattern.CANON_EQ); - - @Override - public boolean apply(String filter, BitbucketCause cause) { - return this.applyByRx(SRC_MATCHER_RX, new OnlySourceFlag(), filter, cause) && - this.applyByRx(DST_MATCHER_RX, new OnlyDestFlag(), filter, cause); - } - @Override - public boolean check(String filter) { - return HasSourceOrDestPartsPredicate(filter); - } -} - -class AuthorFlag extends Filter { - static final Pattern AUTHOR_MATCHER_RX = Pattern.compile(AUTHOR_RX + BRANCH_FILTER_RX_PART, Pattern.CASE_INSENSITIVE | Pattern.CANON_EQ); - - static class AuthorFlagImpl extends Filter { - @Override - public boolean apply(String filter, BitbucketCause cause) { - String selectedRx = filter.startsWith(RX_FILTER_FLAG_SINGLE) ? filter.substring(RX_FILTER_FLAG_SINGLE.length()) : Pattern.quote(filter); - logger.log(Level.FINE, "AuthorFlagImpl using filter: {0}", selectedRx); - Matcher matcher = Pattern.compile(selectedRx, Pattern.CASE_INSENSITIVE).matcher(cause.getPullRequestAuthor()); - return filter.startsWith(RX_FILTER_FLAG_SINGLE) ? matcher.find() : matcher.matches(); - } - @Override - public boolean check(String filter) { return false; } - } - - @Override - public boolean apply(String filter, BitbucketCause cause) { - return this.applyByRx(AUTHOR_MATCHER_RX, new AuthorFlagImpl(), filter, cause); - } - @Override - public boolean check(String filter) { - return HasAuthorPartsPredicate(filter); - } -} - -class CombinedFlags extends Filter { - private final Filter[] _filters; - public CombinedFlags(Filter[] filters) { - _filters = filters; - } - - @Override - public boolean apply(String filter, BitbucketCause cause) { - boolean applied = true; - for(Filter f: _filters) - if (f.check(filter)) - applied = applied && f.apply(filter, cause); - return applied; - } - @Override - public boolean check(String filter) { - for(Filter f: _filters) - if (f.check(filter)) - return true; - return false; - } -} - -/** - * Created by maxvodo - */ -public class BitbucketBuildFilter { - private static final Logger logger = Logger.getLogger(BitbucketBuildTrigger.class.getName()); - - private final String filter; - private Filter currFilter = null; - private static final List AvailableFilters; - - static { - ArrayList filters = new ArrayList(); - - filters.add(new AnyFlag()); - filters.add(new CombinedFlags(new Filter[] { - new SourceDestFlag(), - new AuthorFlag() - })); - filters.add(new OnlyDestFlag()); - filters.add(new EmptyFilter()); - - AvailableFilters = filters; - } - - public BitbucketBuildFilter(String f) { - this.filter = (f != null ? f : "").trim(); - this.buildFilter(this.filter); - } - - private void buildFilter(String filter) { - logger.log(Level.FINE, "Build filter by phrase: {0}", filter); - for(Filter f : AvailableFilters) { - if (f.check(filter)) { - this.currFilter = f; - logger.log(Level.FINE, "Using filter: {0}", f.getClass().getSimpleName()); - break; - } - } - } - - public boolean approved(BitbucketCause cause) { - logger.log(Level.FINE, "Approve cause: {0}", cause.toString()); - return this.currFilter.apply(this.filter, cause); - } - - public static BitbucketBuildFilter instanceByString(String filter) { - logger.log(Level.FINE, "Filter instance by filter string"); - return new BitbucketBuildFilter(filter); - } - - static public String filterFromGitSCMSource(AbstractGitSCMSource gitscm, String defaultFilter) { - if (gitscm == null) { - logger.log(Level.FINE, "Git SCMSource unavailable. Using default value: {0}", defaultFilter); - return defaultFilter; - } - - StringBuffer filter = new StringBuffer(defaultFilter); - final String includes = gitscm.getIncludes(); - if (includes != null && !includes.isEmpty()) { - for(String part : includes.split("\\s+")) { - filter.append(String.format("%s ", part.replaceAll("\\*\\/", "d:"))); - } - } - - logger.log(Level.FINE, "Git includes transformation to filter result: {1} -> {0}; default: {2}", new Object[]{ filter, includes, defaultFilter }); - return filter.toString().trim(); - } - - public static BitbucketBuildFilter instanceBySCM(Collection scmSources, String defaultFilter) { - logger.log(Level.FINE, "Filter instance by using SCMSources list with {0} items", scmSources.size()); - AbstractGitSCMSource gitscm = null; - for(SCMSource scm : scmSources) { - logger.log(Level.FINE, "Check {0} SCMSource ", scm.getClass()); - if (scm instanceof AbstractGitSCMSource) { - gitscm = (AbstractGitSCMSource)scm; - break; - } - } - return new BitbucketBuildFilter(filterFromGitSCMSource(gitscm, defaultFilter)); - } -} diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildListener.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildListener.java deleted file mode 100644 index 230253b..0000000 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildListener.java +++ /dev/null @@ -1,58 +0,0 @@ -package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; - -import hudson.Extension; -import hudson.model.AbstractBuild; -import hudson.model.Job; -import hudson.model.Run; -import hudson.model.TaskListener; -import hudson.model.listeners.RunListener; -import hudson.triggers.Trigger; -import jenkins.model.ParameterizedJobMixIn; - -import javax.annotation.Nonnull; -import java.util.logging.Logger; - -/** - * Created by nishio - */ -@Extension -public class BitbucketBuildListener extends RunListener> { - private static final Logger logger = Logger.getLogger(BitbucketBuildListener.class.getName()); - - @Override - public void onStarted(Run r, TaskListener listener) { - logger.fine("BitbucketBuildListener onStarted called."); - BitbucketBuilds builds = builds(r); - if (builds != null) { - builds.onStarted((BitbucketCause) r.getCause(BitbucketCause.class), r); - } - } - - @Override - public void onCompleted(Run r, @Nonnull TaskListener listener) { - logger.fine("BitbucketBuildListener onCompleted called."); - BitbucketBuilds builds = builds(r); - if (builds != null) { - builds.onCompleted((BitbucketCause) r.getCause(BitbucketCause.class), r.getResult(), r.getUrl()); - } - } - - private BitbucketBuilds builds(Run r) { - BitbucketBuildTrigger trigger = null; - if (r instanceof AbstractBuild) { - trigger = BitbucketBuildTrigger.getTrigger(((AbstractBuild) r).getProject()); - } else { - Job job = r.getParent(); - if (job instanceof ParameterizedJobMixIn.ParameterizedJob) { - - for (Trigger t : ((ParameterizedJobMixIn.ParameterizedJob) job).getTriggers().values()) { - if (t instanceof BitbucketBuildTrigger) { - trigger = (BitbucketBuildTrigger) t; - } - } - } - } - return trigger == null ? null : trigger.getBuilder().getBuilds(); - } - -} diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger.java deleted file mode 100644 index be36382..0000000 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger.java +++ /dev/null @@ -1,332 +0,0 @@ -package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; - -import antlr.ANTLRException; -import com.cloudbees.plugins.credentials.CredentialsProvider; -import com.cloudbees.plugins.credentials.common.StandardListBoxModel; -import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; -import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials; -import hudson.Extension; -import hudson.model.*; -import hudson.model.Queue; -import hudson.model.queue.QueueTaskFuture; -import hudson.plugins.git.RevisionParameterAction; -import hudson.triggers.Trigger; -import hudson.triggers.TriggerDescriptor; -import hudson.util.ListBoxModel; -import jenkins.model.Jenkins; -import jenkins.model.ParameterizedJobMixIn; -import net.sf.json.JSONObject; -import org.apache.commons.lang.StringUtils; -import org.jenkinsci.Symbol; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.StaplerRequest; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import static com.cloudbees.plugins.credentials.CredentialsMatchers.instanceOf; - -/** - * Created by nishio - */ -public class BitbucketBuildTrigger extends Trigger> { - private static final Logger logger = Logger.getLogger(BitbucketBuildTrigger.class.getName()); - private final String projectPath; - private final String cron; - private final String credentialsId; - private final String username; - private final String password; - private final String repositoryOwner; - private final String repositoryName; - private final String branchesFilter; - private final boolean branchesFilterBySCMIncludes; - private final String ciKey; - private final String ciName; - private final String ciSkipPhrases; - private final boolean checkDestinationCommit; - private final boolean approveIfSuccess; - private final boolean cancelOutdatedJobs; - private final String commentTrigger; - - transient private BitbucketPullRequestsBuilder bitbucketPullRequestsBuilder; - - public static final BitbucketBuildTriggerDescriptor descriptor = new BitbucketBuildTriggerDescriptor(); - - @DataBoundConstructor - public BitbucketBuildTrigger( - String projectPath, - String cron, - String credentialsId, - String username, - String password, - String repositoryOwner, - String repositoryName, - String branchesFilter, - boolean branchesFilterBySCMIncludes, - String ciKey, - String ciName, - String ciSkipPhrases, - boolean checkDestinationCommit, - boolean approveIfSuccess, - boolean cancelOutdatedJobs, - String commentTrigger - ) throws ANTLRException { - super(cron); - this.projectPath = projectPath; - this.cron = cron; - this.credentialsId = credentialsId; - this.username = username; - this.password = password; - this.repositoryOwner = repositoryOwner; - this.repositoryName = repositoryName; - this.branchesFilter = branchesFilter; - this.branchesFilterBySCMIncludes = branchesFilterBySCMIncludes; - this.ciKey = ciKey; - this.ciName = ciName; - this.ciSkipPhrases = ciSkipPhrases; - this.checkDestinationCommit = checkDestinationCommit; - this.approveIfSuccess = approveIfSuccess; - this.cancelOutdatedJobs = cancelOutdatedJobs; - this.commentTrigger = commentTrigger; - } - - public String getProjectPath() { - return this.projectPath; - } - - public String getCron() { - return this.cron; - } - - public String getCredentialsId() { - return credentialsId; - } - - public String getUsername() { - return username; - } - - public String getPassword() { - return password; - } - - public String getRepositoryOwner() { - return repositoryOwner; - } - - public String getRepositoryName() { - return repositoryName; - } - - public String getBranchesFilter() { - return branchesFilter; - } - - public boolean getBranchesFilterBySCMIncludes() { - return branchesFilterBySCMIncludes; - } - - public String getCiKey() { - return ciKey; - } - - public String getCiName() { - return ciName; - } - - public String getCiSkipPhrases() { - return ciSkipPhrases; - } - - public boolean getCheckDestinationCommit() { - return checkDestinationCommit; - } - - public boolean getApproveIfSuccess() { - return approveIfSuccess; - } - - public boolean getCancelOutdatedJobs() { - return cancelOutdatedJobs; - } - /** - * @return a phrase that when entered in a comment will trigger a new build - */ - public String getCommentTrigger() { - return commentTrigger; - } - - @Override - public void start(Job project, boolean newInstance) { - try { - this.bitbucketPullRequestsBuilder = BitbucketPullRequestsBuilder.getBuilder(); - this.bitbucketPullRequestsBuilder.setProject(project); - this.bitbucketPullRequestsBuilder.setTrigger(this); - this.bitbucketPullRequestsBuilder.setupBuilder(); - } catch(IllegalStateException e) { - logger.log(Level.SEVERE, "Can't start trigger", e); - return; - } - super.start(project, newInstance); - } - - public static BitbucketBuildTrigger getTrigger(AbstractProject project) { - Trigger trigger = project.getTrigger(BitbucketBuildTrigger.class); - return (BitbucketBuildTrigger)trigger; - } - - public BitbucketPullRequestsBuilder getBuilder() { - return this.bitbucketPullRequestsBuilder; - } - - private ParameterizedJobMixIn retrieveScheduleJob(final Job job) { - // TODO 1.621+ use standard method - return new ParameterizedJobMixIn() { - @Override - protected Job asJob() { - return job; - } - }; - } - - public QueueTaskFuture startJob(BitbucketCause cause) { - Map values = this.getDefaultParameters(); - - if (getCancelOutdatedJobs()) { - cancelPreviousJobsInQueueThatMatch(cause); - abortRunningJobsThatMatch(cause); - } - - return retrieveScheduleJob(this.job).scheduleBuild2(0, - new CauseAction(cause), - new ParametersAction(new ArrayList(values.values())), - new RevisionParameterAction(cause.getSourceCommitHash())); - } - - private void cancelPreviousJobsInQueueThatMatch(@Nonnull BitbucketCause bitbucketCause) { - logger.fine("Looking for queued jobs that match PR ID: " + bitbucketCause.getPullRequestId()); - Queue queue = getInstance().getQueue(); - - for (Queue.Item item : queue.getItems()) { - if (hasCauseFromTheSamePullRequest(item.getCauses(), bitbucketCause)) { - logger.fine("Canceling item in queue: " + item); - queue.cancel(item); - } - } - } - - private Jenkins getInstance() { - final Jenkins instance = Jenkins.getInstance(); - if (instance == null){ - throw new IllegalStateException("Jenkins instance is NULL!"); - } - return instance; - } - - private void abortRunningJobsThatMatch(@Nonnull BitbucketCause bitbucketCause) { - logger.fine("Looking for running jobs that match PR ID: " + bitbucketCause.getPullRequestId()); - for (Object o : job.getBuilds()) { - if (o instanceof Run) { - Run build = (Run) o; - if (build.isBuilding() && hasCauseFromTheSamePullRequest(build.getCauses(), bitbucketCause)) { - logger.fine("Aborting build: " + build + " since PR is outdated"); - setBuildDescription(build); - final Executor executor = build.getExecutor(); - if (executor == null){ - throw new IllegalStateException("Executor can't be NULL"); - } - executor.interrupt(Result.ABORTED); - } - } - } - } - - private void setBuildDescription(final Run build) { - try { - build.setDescription("Aborting build by `Bitbucket Pullrequest Builder Plugin`: " + build + " since PR is outdated"); - } catch (IOException e) { - logger.warning("Can't set up build description due to an IOException: " + e.getMessage()); - } - } - - private boolean hasCauseFromTheSamePullRequest(@Nullable List causes, @Nullable BitbucketCause pullRequestCause) { - if (causes != null && pullRequestCause != null) { - for (Cause cause : causes) { - if (cause instanceof BitbucketCause) { - BitbucketCause sc = (BitbucketCause) cause; - if (StringUtils.equals(sc.getPullRequestId(), pullRequestCause.getPullRequestId()) && - StringUtils.equals(sc.getRepositoryName(), pullRequestCause.getRepositoryName())) { - return true; - } - } - } - } - return false; - } - - private Map getDefaultParameters() { - Map values = new HashMap(); - ParametersDefinitionProperty definitionProperty = this.job.getProperty(ParametersDefinitionProperty.class); - - if (definitionProperty != null) { - for (ParameterDefinition definition : definitionProperty.getParameterDefinitions()) { - values.put(definition.getName(), definition.getDefaultParameterValue()); - } - } - return values; - } - - @Override - public void run() { - Job project = this.getBuilder().getProject(); - if (project instanceof AbstractProject && ((AbstractProject)project).isDisabled()) { - logger.fine("Build Skip."); - } else { - this.bitbucketPullRequestsBuilder.run(); - this.getDescriptor().save(); - } - } - - @Override - public void stop() { - super.stop(); - } - - @Extension - @Symbol("bitbucketpr") - public static final class BitbucketBuildTriggerDescriptor extends TriggerDescriptor { - public BitbucketBuildTriggerDescriptor() { - load(); - } - - @Override - public boolean isApplicable(Item item) { - return item instanceof Job && item instanceof ParameterizedJobMixIn.ParameterizedJob; - } - - @Override - public String getDisplayName() { - return "Bitbucket Pull Requests Builder"; - } - - @Override - public boolean configure(StaplerRequest req, JSONObject json) throws FormException { - save(); - return super.configure(req, json); - } - - public ListBoxModel doFillCredentialsIdItems() { - return new StandardListBoxModel() - .withEmptySelection() - .withMatching(instanceOf(UsernamePasswordCredentials.class), - CredentialsProvider.lookupCredentials(StandardUsernamePasswordCredentials.class)); - } - } -} diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuilds.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuilds.java deleted file mode 100644 index 6ba3dab..0000000 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuilds.java +++ /dev/null @@ -1,54 +0,0 @@ -package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; - -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.BuildState; -import hudson.model.*; -import jenkins.model.JenkinsLocationConfiguration; - -import java.io.IOException; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Created by nishio - */ -public class BitbucketBuilds { - private static final Logger logger = Logger.getLogger(BitbucketBuilds.class.getName()); - private BitbucketBuildTrigger trigger; - private BitbucketRepository repository; - - public BitbucketBuilds(BitbucketBuildTrigger trigger, BitbucketRepository repository) { - this.trigger = trigger; - this.repository = repository; - this.repository.init(); - } - - void onStarted(BitbucketCause cause, Run build) { - if (cause == null) { - return; - } - try { - build.setDescription(cause.getShortDescription()); - } catch (IOException e) { - logger.log(Level.SEVERE, "Can't update build description", e); - } - } - - void onCompleted(BitbucketCause cause, Result result, String buildUrl) { - if (cause == null) { - return; - } - JenkinsLocationConfiguration globalConfig = new JenkinsLocationConfiguration(); - String rootUrl = globalConfig.getUrl(); - if (rootUrl == null) { - logger.warning("PLEASE SET JENKINS ROOT URL IN GLOBAL CONFIGURATION FOR BUILD STATE REPORTING"); - } else { - buildUrl = rootUrl + buildUrl; - BuildState state = result == Result.SUCCESS ? BuildState.SUCCESSFUL : BuildState.FAILED; - repository.setBuildStatus(cause, state, buildUrl); - } - - if (this.trigger.getApproveIfSuccess() && result == Result.SUCCESS) { - this.repository.postPullRequestApproval(cause.getPullRequestId()); - } - } -} diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketCause.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketCause.java deleted file mode 100644 index 3cb107b..0000000 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketCause.java +++ /dev/null @@ -1,93 +0,0 @@ -package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; - -import hudson.model.Cause; - -/** - * Created by nishio - */ -public class BitbucketCause extends Cause { - private final String sourceBranch; - private final String targetBranch; - private final String repositoryOwner; - private final String repositoryName; - private final String pullRequestId; - private final String destinationRepositoryOwner; - private final String destinationRepositoryName; - private final String pullRequestTitle; - private final String sourceCommitHash; - private final String destinationCommitHash; - private final String pullRequestAuthor; - public static final String BITBUCKET_URL = "https://bitbucket.org/"; - - public BitbucketCause(String sourceBranch, - String targetBranch, - String repositoryOwner, - String repositoryName, - String pullRequestId, - String destinationRepositoryOwner, - String destinationRepositoryName, - String pullRequestTitle, - String sourceCommitHash, - String destinationCommitHash, - String pullRequestAuthor) { - this.sourceBranch = sourceBranch; - this.targetBranch = targetBranch; - this.repositoryOwner = repositoryOwner; - this.repositoryName = repositoryName; - this.pullRequestId = pullRequestId; - this.destinationRepositoryOwner = destinationRepositoryOwner; - this.destinationRepositoryName = destinationRepositoryName; - this.pullRequestTitle = pullRequestTitle; - this.sourceCommitHash = sourceCommitHash; - this.destinationCommitHash = destinationCommitHash; - this.pullRequestAuthor = pullRequestAuthor; - } - - public String getSourceBranch() { - return sourceBranch; - } - public String getTargetBranch() { - return targetBranch; - } - - public String getRepositoryOwner() { - return repositoryOwner; - } - - public String getRepositoryName() { - return repositoryName; - } - - public String getPullRequestId() { - return pullRequestId; - } - - - public String getDestinationRepositoryOwner() { - return destinationRepositoryOwner; - } - - public String getDestinationRepositoryName() { - return destinationRepositoryName; - } - - public String getPullRequestTitle() { - return pullRequestTitle; - } - - public String getSourceCommitHash() { return sourceCommitHash; } - - public String getDestinationCommitHash() { return destinationCommitHash; } - - @Override - public String getShortDescription() { - String description = "#" + this.getPullRequestId() + " " + this.getPullRequestTitle() + ""; - return description; - } - - public String getPullRequestAuthor() { - return this.pullRequestAuthor; - } -} diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketPullRequestsBuilder.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketPullRequestsBuilder.java deleted file mode 100644 index 5b37fea..0000000 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketPullRequestsBuilder.java +++ /dev/null @@ -1,87 +0,0 @@ -package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; - -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.Pullrequest; - -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import java.util.Collection; -import java.util.logging.Level; -import java.util.logging.Logger; - -import hudson.model.Job; -import org.apache.commons.codec.binary.Hex; - -/** - * Created by nishio - */ -public class BitbucketPullRequestsBuilder { - private static final Logger logger = Logger.getLogger(BitbucketBuildTrigger.class.getName()); - private Job project; - private BitbucketBuildTrigger trigger; - private BitbucketRepository repository; - private BitbucketBuilds builds; - - public static BitbucketPullRequestsBuilder getBuilder() { - return new BitbucketPullRequestsBuilder(); - } - - public void stop() { - // TODO? - } - - public void run() { - logger.fine("Build Start."); - this.repository.init(); - Collection targetPullRequests = this.repository.getTargetPullRequests(); - this.repository.addFutureBuildTasks(targetPullRequests); - } - - public BitbucketPullRequestsBuilder setupBuilder() { - if (this.project == null || this.trigger == null) { - throw new IllegalStateException(); - } - this.repository = new BitbucketRepository(this.trigger.getProjectPath(), this); - this.repository.init(); - this.builds = new BitbucketBuilds(this.trigger, this.repository); - return this; - } - - public void setProject(Job project) { - this.project = project; - } - - public void setTrigger(BitbucketBuildTrigger trigger) { - this.trigger = trigger; - } - - public Job getProject() { - return this.project; - } - - /** - * Return MD5 hashed full project name or full project name, if MD5 hash provider inaccessible - * @return unique project id - */ - public String getProjectId() { - try { - final MessageDigest MD5 = MessageDigest.getInstance("MD5"); - return new String(Hex.encodeHex(MD5.digest(this.project.getFullName().getBytes("UTF-8")))); - } catch (NoSuchAlgorithmException exc) { - logger.log(Level.WARNING, "Failed to produce hash", exc); - } catch (UnsupportedEncodingException exc) { - logger.log(Level.WARNING, "Failed to produce hash", exc); - } - return this.project.getFullName(); - - } - - public BitbucketBuildTrigger getTrigger() { - return this.trigger; - } - - public BitbucketBuilds getBuilds() { - return this.builds; - } -} diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketRepository.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketRepository.java deleted file mode 100644 index 3b0a314..0000000 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketRepository.java +++ /dev/null @@ -1,346 +0,0 @@ -package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.logging.Logger; - -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.ApiClient; -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.BuildState; -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.Pullrequest; - -import java.util.LinkedList; -import java.util.logging.Level; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import com.cloudbees.plugins.credentials.CredentialsMatchers; -import com.cloudbees.plugins.credentials.CredentialsProvider; -import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; -import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials; - -import jenkins.model.Jenkins; -import jenkins.scm.api.SCMSource; -import jenkins.scm.api.SCMSourceOwner; -import jenkins.scm.api.SCMSourceOwners; - -import org.apache.commons.lang.StringUtils; - -import static com.cloudbees.plugins.credentials.CredentialsMatchers.instanceOf; - -/** - * Created by nishio - */ -public class BitbucketRepository { - private static final Logger logger = Logger.getLogger(BitbucketRepository.class.getName()); - private static final String BUILD_DESCRIPTION = "%s: %s into %s"; - private static final String BUILD_REQUEST_DONE_MARKER = "ttp build flag"; - private static final String BUILD_REQUEST_MARKER_TAG_SINGLE_RX = "\\#[\\w\\-\\d]+"; - private static final String BUILD_REQUEST_MARKER_TAGS_RX = "\\[bid\\:\\s?(.*)\\]"; - /** - * Default value for comment trigger. - */ - public static final String DEFAULT_COMMENT_TRIGGER = "test this please"; - - private String projectPath; - private BitbucketPullRequestsBuilder builder; - private BitbucketBuildTrigger trigger; - private ApiClient client; - - public BitbucketRepository(String projectPath, BitbucketPullRequestsBuilder builder) { - this.projectPath = projectPath; - this.builder = builder; - } - - public void init() { - this.init(null, null); - } - - public void init(T httpFactory) { - this.init(null, httpFactory); - } - - public void init(ApiClient client) { - this.init(client, null); - } - - public void init(ApiClient client, T httpFactory) { - this.trigger = this.builder.getTrigger(); - - if (client == null) { - String username = trigger.getUsername(); - String password = trigger.getPassword(); - StandardUsernamePasswordCredentials credentials = getCredentials(trigger.getCredentialsId()); - if (credentials != null) { - username = credentials.getUsername(); - password = credentials.getPassword().getPlainText(); - } - this.client = new ApiClient( - username, - password, - trigger.getRepositoryOwner(), - trigger.getRepositoryName(), - trigger.getCiKey(), - trigger.getCiName(), - httpFactory - ); - - } else this.client = client; - } - - public Collection getTargetPullRequests() { - logger.fine("Fetch PullRequests."); - List pullRequests = client.getPullRequests(); - List targetPullRequests = new ArrayList(); - for(Pullrequest pullRequest : pullRequests) { - if (isBuildTarget(pullRequest)) { - targetPullRequests.add(pullRequest); - } - } - return targetPullRequests; - } - - public ApiClient getClient() { - return this.client; - } - - public void addFutureBuildTasks(Collection pullRequests) { - for(Pullrequest pullRequest : pullRequests) { - if ( this.trigger.getApproveIfSuccess() ) { - deletePullRequestApproval(pullRequest.getId()); - } - BitbucketCause cause = new BitbucketCause( - pullRequest.getSource().getBranch().getName(), - pullRequest.getDestination().getBranch().getName(), - pullRequest.getSource().getRepository().getOwnerName(), - pullRequest.getSource().getRepository().getRepositoryName(), - pullRequest.getId(), - pullRequest.getDestination().getRepository().getOwnerName(), - pullRequest.getDestination().getRepository().getRepositoryName(), - pullRequest.getTitle(), - pullRequest.getSource().getCommit().getHash(), - pullRequest.getDestination().getCommit().getHash(), - pullRequest.getAuthor().getCombinedUsername() - ); - setBuildStatus(cause, BuildState.INPROGRESS, getInstance().getRootUrl()); - this.builder.getTrigger().startJob(cause); - } - } - - private Jenkins getInstance() { - final Jenkins instance = Jenkins.getInstance(); - if (instance == null){ - throw new IllegalStateException("Jenkins instance is NULL!"); - } - return instance; - } - - - public void setBuildStatus(BitbucketCause cause, BuildState state, String buildUrl) { - String comment = null; - String sourceCommit = cause.getSourceCommitHash(); - String owner = cause.getRepositoryOwner(); - String repository = cause.getRepositoryName(); - String destinationBranch = cause.getTargetBranch(); - - logger.fine("setBuildStatus " + state + " for commit: " + sourceCommit + " with url " + buildUrl); - - if (state == BuildState.FAILED || state == BuildState.SUCCESSFUL) { - comment = String.format(BUILD_DESCRIPTION, builder.getProject().getDisplayName(), sourceCommit, destinationBranch); - } - - this.client.setBuildStatus(owner, repository, sourceCommit, state, buildUrl, comment, this.builder.getProjectId()); - } - - public void deletePullRequestApproval(String pullRequestId) { - this.client.deletePullRequestApproval(pullRequestId); - } - - public void postPullRequestApproval(String pullRequestId) { - this.client.postPullRequestApproval(pullRequestId); - } - - public String getMyBuildTag(String buildKey) { - return "#" + this.client.buildStatusKey(buildKey); - } - - final static Pattern BUILD_TAGS_RX = Pattern.compile(BUILD_REQUEST_MARKER_TAGS_RX, Pattern.CASE_INSENSITIVE | Pattern.CANON_EQ); - final static Pattern SINGLE_BUILD_TAG_RX = Pattern.compile(BUILD_REQUEST_MARKER_TAG_SINGLE_RX, Pattern.CASE_INSENSITIVE | Pattern.CANON_EQ); - final static String CONTENT_PART_TEMPLATE = "```[bid: %s]```"; - - private List getAvailableBuildTagsFromTTPComment(String buildTags) { - logger.log(Level.FINE, "Parse {0}", new Object[]{ buildTags }); - List availableBuildTags = new LinkedList(); - Matcher subBuildTagMatcher = SINGLE_BUILD_TAG_RX.matcher(buildTags); - while(subBuildTagMatcher.find()) availableBuildTags.add(subBuildTagMatcher.group(0).trim()); - return availableBuildTags; - } - - public boolean hasMyBuildTagInTTPComment(String content, String buildKey) { - Matcher tagsMatcher = BUILD_TAGS_RX.matcher(content); - if (tagsMatcher.find()) { - logger.log(Level.FINE, "Content {0} g[1]:{1} mykey:{2}", new Object[] { content, tagsMatcher.group(1).trim(), this.getMyBuildTag(buildKey) }); - return this.getAvailableBuildTagsFromTTPComment(tagsMatcher.group(1).trim()).contains(this.getMyBuildTag(buildKey)); - } - else return false; - } - - private void postBuildTagInTTPComment(String pullRequestId, String content, String buildKey) { - logger.log(Level.FINE, "Update build tag for {0} build key", buildKey); - List builds = this.getAvailableBuildTagsFromTTPComment(content); - builds.add(this.getMyBuildTag(buildKey)); - content += " " + String.format(CONTENT_PART_TEMPLATE, StringUtils.join(builds, " ")); - logger.log(Level.FINE, "Post comment: {0} with original content {1}", new Object[]{ content, this.client.postPullRequestComment(pullRequestId, content).getId() }); - } - - private boolean isTTPComment(String content) { - // special case: in unit tests, trigger is null and can't be mocked - String commentTrigger = DEFAULT_COMMENT_TRIGGER; - if(trigger != null && StringUtils.isNotBlank(trigger.getCommentTrigger())) { - commentTrigger = trigger.getCommentTrigger(); - } - return content.toLowerCase().contains(commentTrigger); - } - - private boolean isTTPCommentBuildTags(String content) { - return content.toLowerCase().contains(BUILD_REQUEST_DONE_MARKER.toLowerCase()); - } - - public List filterPullRequestComments(List comments) { - logger.fine("Filter PullRequest Comments."); - Collections.sort(comments); - Collections.reverse(comments); - List filteredComments = new LinkedList(); - for(Pullrequest.Comment comment : comments) { - String content = comment.getContent(); - if (content == null || content.isEmpty()) continue; - boolean isTTP = this.isTTPComment(content); - boolean isTTPBuild = this.isTTPCommentBuildTags(content); - if (isTTP || isTTPBuild) filteredComments.add(comment); - if (isTTP) break; - } - return filteredComments; - } - - private boolean isBuildTarget(Pullrequest pullRequest) { - if (pullRequest.getState() != null && pullRequest.getState().equals("OPEN")) { - if (isSkipBuild(pullRequest.getTitle()) || !isFilteredBuild(pullRequest)) { - return false; - } - - Pullrequest.Revision source = pullRequest.getSource(); - String sourceCommit = source.getCommit().getHash(); - Pullrequest.Revision destination = pullRequest.getDestination(); - String owner = destination.getRepository().getOwnerName(); - String repositoryName = destination.getRepository().getRepositoryName(); - - Pullrequest.Repository sourceRepository = source.getRepository(); - String buildKeyPart = this.builder.getProjectId(); - - final boolean commitAlreadyBeenProcessed = this.client.hasBuildStatus( - sourceRepository.getOwnerName(), sourceRepository.getRepositoryName(), sourceCommit, buildKeyPart - ); - if (commitAlreadyBeenProcessed) logger.log(Level.FINE, - "Commit {0}#{1} has already been processed", - new Object[]{ sourceCommit, buildKeyPart } - ); - - final String id = pullRequest.getId(); - List comments = client.getPullRequestComments(owner, repositoryName, id); - - boolean rebuildCommentAvailable = false; - if (comments != null) { - Collection filteredComments = this.filterPullRequestComments(comments); - boolean hasMyBuildTag = false; - for (Pullrequest.Comment comment : filteredComments) { - String content = comment.getContent(); - if (this.isTTPComment(content)) { - rebuildCommentAvailable = true; - logger.log(Level.FINE, - "Rebuild comment available for commit {0} and comment #{1}", - new Object[]{ sourceCommit, comment.getId() } - ); - } - if (isTTPCommentBuildTags(content)) - hasMyBuildTag |= this.hasMyBuildTagInTTPComment(content, buildKeyPart); - } - rebuildCommentAvailable &= !hasMyBuildTag; - } - if (rebuildCommentAvailable) this.postBuildTagInTTPComment(id, "TTP build flag", buildKeyPart); - - final boolean canBuildTarget = rebuildCommentAvailable || !commitAlreadyBeenProcessed; - logger.log(Level.FINE, "Build target? {0} [rebuild:{1} processed:{2}]", new Object[]{ canBuildTarget, rebuildCommentAvailable, commitAlreadyBeenProcessed}); - return canBuildTarget; - } - - return false; - } - - private boolean isSkipBuild(String pullRequestTitle) { - String skipPhrases = this.trigger.getCiSkipPhrases(); - if (skipPhrases != null && !"".equals(skipPhrases)) { - String[] phrases = skipPhrases.split(","); - for(String phrase : phrases) { - if (pullRequestTitle.toLowerCase().contains(phrase.trim().toLowerCase())) { - return true; - } - } - } - return false; - } - - private boolean isFilteredBuild(Pullrequest pullRequest) { - - final String pullRequestId = pullRequest.getId(); - final String pullRequestTitle = pullRequest.getTitle(); - final String destinationRepoName = pullRequest.getDestination().getRepository().getRepositoryName(); - - // pullRequest.getDestination().getCommit() may return null for pull requests with merge conflicts - // * see: https://github.com/nishio-dens/bitbucket-pullrequest-builder-plugin/issues/119 - // * see: https://github.com/nishio-dens/bitbucket-pullrequest-builder-plugin/issues/98 - final String destinationCommitHash; - if (pullRequest.getDestination().getCommit() == null) { - logger.log(Level.INFO, "Pull request #{0} ''{1}'' in repo ''{2}'' has a null value for destination commit.", - new Object[]{pullRequestId, pullRequestTitle, destinationRepoName}); - destinationCommitHash = null; - } else { - destinationCommitHash = pullRequest.getDestination().getCommit().getHash(); - } - - BitbucketCause cause = new BitbucketCause( - pullRequest.getSource().getBranch().getName(), - pullRequest.getDestination().getBranch().getName(), - pullRequest.getSource().getRepository().getOwnerName(), - pullRequest.getSource().getRepository().getRepositoryName(), - pullRequestId, - pullRequest.getDestination().getRepository().getOwnerName(), - destinationRepoName, - pullRequestTitle, - pullRequest.getSource().getCommit().getHash(), - destinationCommitHash, - pullRequest.getAuthor().getCombinedUsername() - ); - - //@FIXME: Way to iterate over all available SCMSources - List sources = new LinkedList(); - for(SCMSourceOwner owner : SCMSourceOwners.all()) - for(SCMSource src : owner.getSCMSources()) - sources.add(src); - - BitbucketBuildFilter filter = !this.trigger.getBranchesFilterBySCMIncludes() ? - BitbucketBuildFilter.instanceByString(this.trigger.getBranchesFilter()) : - BitbucketBuildFilter.instanceBySCM(sources, this.trigger.getBranchesFilter()); - - return filter.approved(cause); - } - - private StandardUsernamePasswordCredentials getCredentials(String credentialsId) { - if (null == credentialsId) return null; - return CredentialsMatchers - .firstOrNull( - CredentialsProvider.lookupCredentials(StandardUsernamePasswordCredentials.class), - CredentialsMatchers.allOf(CredentialsMatchers.withId(credentialsId), - instanceOf(UsernamePasswordCredentials.class))); - } -} diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/ApiClient.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/ApiClient.java deleted file mode 100644 index 0c21806..0000000 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/ApiClient.java +++ /dev/null @@ -1,285 +0,0 @@ -package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket; - -import org.apache.commons.httpclient.*; -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.DeleteMethod; -import org.apache.commons.httpclient.params.HttpClientParams; -import org.codehaus.jackson.map.ObjectMapper; -import org.codehaus.jackson.map.type.TypeFactory; -import org.codehaus.jackson.type.JavaType; -import org.codehaus.jackson.type.TypeReference; - -import java.io.IOException; -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 hudson.ProxyConfiguration; - -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.httpclient.methods.PutMethod; -import org.apache.commons.httpclient.util.EncodingUtil; - -/** - * Created by nishio - */ -public class ApiClient { - private static final Logger logger = Logger.getLogger(ApiClient.class.getName()); - private static final String V1_API_BASE_URL = "https://bitbucket.org/api/1.0/repositories/"; - private static final String V2_API_BASE_URL = "https://bitbucket.org/api/2.0/repositories/"; - private static final String COMPUTED_KEY_FORMAT = "%s-%s"; - private String owner; - private String repositoryName; - private Credentials credentials; - private String key; - private String name; - private HttpClientFactory factory; - - public static final byte MAX_KEY_SIZE_BB_API = 40; - - public static class HttpClientFactory { - public static final HttpClientFactory INSTANCE = new HttpClientFactory(); - private static final int DEFAULT_TIMEOUT = 60000; - - public HttpClient getInstanceHttpClient() { - HttpClient client = new HttpClient(); - - HttpClientParams params = client.getParams(); - params.setConnectionManagerTimeout(DEFAULT_TIMEOUT); - params.setSoTimeout(DEFAULT_TIMEOUT); - - if (Jenkins.getInstance() == null) return client; - - ProxyConfiguration proxy = getInstance().proxy; - if (proxy == null) return client; - - logger.log(Level.FINE, "Jenkins proxy: {0}:{1}", new Object[]{ proxy.name, proxy.port }); - client.getHostConfiguration().setProxy(proxy.name, proxy.port); - String username = proxy.getUserName(); - String password = proxy.getPassword(); - - // Consider it to be passed if username specified. Sufficient? - if (username != null && !"".equals(username.trim())) { - logger.log(Level.FINE, "Using proxy authentication (user={0})", username); - client.getState().setProxyCredentials(AuthScope.ANY, - new UsernamePasswordCredentials(username, password)); - } - - return client; - } - - private Jenkins getInstance() { - final Jenkins instance = Jenkins.getInstance(); - if (instance == null){ - throw new IllegalStateException("Jenkins instance is NULL!"); - } - return instance; - } - } - - - - public ApiClient( - String username, String password, - String owner, String repositoryName, - String key, String name, - T httpFactory - ) { - this.credentials = new UsernamePasswordCredentials(username, password); - this.owner = owner; - this.repositoryName = repositoryName; - this.key = key; - this.name = name; - this.factory = httpFactory != null ? httpFactory : HttpClientFactory.INSTANCE; - } - - public List getPullRequests() { - return getAllValues(v2("/pullrequests/"), 50, Pullrequest.class); - } - - public List getPullRequestComments(String commentOwnerName, String commentRepositoryName, String pullRequestId) { - return getAllValues(v2("/pullrequests/" + pullRequestId + "/comments"), 100, Pullrequest.Comment.class); - } - - public String getName() { - return this.name; - } - - private static MessageDigest SHA1 = null; - - /** - * Retrun - * @param keyExPart - * @return key parameter for call BitBucket API - */ - private String computeAPIKey(String keyExPart) { - String computedKey = String.format(COMPUTED_KEY_FORMAT, this.key, keyExPart); - - if (computedKey.length() > MAX_KEY_SIZE_BB_API) { - try { - if (SHA1 == null) SHA1 = MessageDigest.getInstance("SHA1"); - return new String(Hex.encodeHex(SHA1.digest(computedKey.getBytes("UTF-8")))); - } catch(NoSuchAlgorithmException e) { - logger.log(Level.WARNING, "Failed to create hash provider", e); - } catch (UnsupportedEncodingException e) { - logger.log(Level.WARNING, "Failed to create hash provider", e); - } - } - return (computedKey.length() <= MAX_KEY_SIZE_BB_API) ? computedKey : computedKey.substring(0, MAX_KEY_SIZE_BB_API); - } - - public String buildStatusKey(String bsKey) { - return this.computeAPIKey(bsKey); - } - - public boolean hasBuildStatus(String owner, String repositoryName, String revision, String keyEx) { - String url = v2(owner, repositoryName, "/commit/" + revision + "/statuses/build/" + this.computeAPIKey(keyEx)); - String reqBody = get(url); - return reqBody != null && reqBody.contains("\"state\""); - } - - public void setBuildStatus(String owner, String repositoryName, String revision, BuildState state, String buildUrl, String comment, String keyEx) { - String url = v2(owner, repositoryName, "/commit/" + revision + "/statuses/build"); - String computedKey = this.computeAPIKey(keyEx); - NameValuePair[] data = new NameValuePair[]{ - new NameValuePair("description", comment), - new NameValuePair("key", computedKey), - new NameValuePair("name", this.name), - new NameValuePair("state", state.toString()), - new NameValuePair("url", buildUrl), - }; - logger.log(Level.FINE, "POST state {0} to {1} with key {2} with response {3}", new Object[]{ - state, url, computedKey, post(url, data)} - ); - } - - public void deletePullRequestApproval(String pullRequestId) { - delete(v2("/pullrequests/" + pullRequestId + "/approve")); - } - - public void deletePullRequestComment(String pullRequestId, String commentId) { - delete(v1("/pullrequests/" + pullRequestId + "/comments/" + commentId)); - } - - public void updatePullRequestComment(String pullRequestId, String content, String commentId) { - NameValuePair[] data = new NameValuePair[] { - new NameValuePair("content", content), - }; - put(v1("/pullrequests/" + pullRequestId + "/comments/" + commentId), data); - } - - public Pullrequest.Participant postPullRequestApproval(String pullRequestId) { - try { - return parse(post(v2("/pullrequests/" + pullRequestId + "/approve"), - new NameValuePair[]{}), Pullrequest.Participant.class); - } catch (IOException e) { - logger.log(Level.WARNING, "Invalid pull request approval response.", e); - } - return null; - } - - public Pullrequest.Comment postPullRequestComment(String pullRequestId, String content) { - NameValuePair[] data = new NameValuePair[] { - new NameValuePair("content", content), - }; - try { - return parse(post(v1("/pullrequests/" + pullRequestId + "/comments"), data), new TypeReference() {}); - } catch(Exception e) { - logger.log(Level.WARNING, "Invalid pull request comment response.", e); - } - return null; - } - - private List getAllValues(String rootUrl, int pageLen, Class cls) { - List values = new ArrayList(); - try { - String url = rootUrl + "?pagelen=" + pageLen; - do { - final JavaType type = TypeFactory.defaultInstance().constructParametricType(Pullrequest.Response.class, cls); - Pullrequest.Response response = parse(get(url), type); - values.addAll(response.getValues()); - url = response.getNext(); - } while (url != null); - } catch (Exception e) { - logger.log(Level.WARNING, "invalid response.", e); - } - return values; - } - - private HttpClient getHttpClient() { - return this.factory.getInstanceHttpClient(); - } - - private String v1(String url) { - return V1_API_BASE_URL + this.owner + "/" + this.repositoryName + url; - } - - private String v2(String path) { - return v2(this.owner, this.repositoryName, path); - } - - private String v2(String owner, String repositoryName, String path) { - return V2_API_BASE_URL + owner + "/" + repositoryName + path; - } - - private String get(String path) { - return send(new GetMethod(path)); - } - - private String post(String path, NameValuePair[] data) { - PostMethod req = new PostMethod(path); - req.setRequestBody(data); - req.getParams().setContentCharset("utf-8"); - return send(req); - } - - private void delete(String path) { - send(new DeleteMethod(path)); - } - - private void put(String path, NameValuePair[] data) { - PutMethod req = new PutMethod(path); - req.setRequestBody(EncodingUtil.formUrlEncode(data, "utf-8")); - req.getParams().setContentCharset("utf-8"); - send(req); - } - - private String send(HttpMethodBase req) { - HttpClient client = getHttpClient(); - client.getState().setCredentials(AuthScope.ANY, credentials); - client.getParams().setAuthenticationPreemptive(true); - try { - int statusCode = client.executeMethod(req); - if (statusCode != HttpStatus.SC_OK) { - logger.log(Level.WARNING, "Response status: " + req.getStatusLine()+" URI: "+req.getURI()); - }else{ - return req.getResponseBodyAsString(); - } - } catch (HttpException e) { - logger.log(Level.WARNING, "Failed to send request.", e); - } catch (IOException e) { - logger.log(Level.WARNING, "Failed to send request.", e); - } finally { - req.releaseConnection(); - } - return null; - } - - private R parse(String response, Class cls) throws IOException { - return new ObjectMapper().readValue(response, cls); - } - private R parse(String response, JavaType type) throws IOException { - return new ObjectMapper().readValue(response, type); - } - private R parse(String response, TypeReference ref) throws IOException { - return new ObjectMapper().readValue(response, ref); - } -} diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/BuildState.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/BuildState.java deleted file mode 100644 index a3ef1f1..0000000 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/BuildState.java +++ /dev/null @@ -1,10 +0,0 @@ -package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket; - -/** - * Valid build states for a pull request - * - * @see "https://confluence.atlassian.com/bitbucket/buildstatus-resource-779295267.html" - */ -public enum BuildState { - FAILED, INPROGRESS, SUCCESSFUL -} diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/Pullrequest.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/Pullrequest.java deleted file mode 100644 index b69805e..0000000 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/Pullrequest.java +++ /dev/null @@ -1,392 +0,0 @@ -package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket; - -import java.util.List; -import java.util.Comparator; -import java.util.Map; - -import org.codehaus.jackson.annotate.JsonIgnoreProperties; -import org.codehaus.jackson.annotate.JsonProperty; - -/** - * POJOs representing the pull-requests extracted from the - * JSON response of the Bitbucket API V2. - * - * @see "https://confluence.atlassian.com/bitbucket/pullrequests-resource-423626332.html#pullrequestsResource-GETaspecificpullrequest" - */ - -@JsonIgnoreProperties(ignoreUnknown = true) -public class Pullrequest { - - private String description; - private Boolean closeSourceBranch; - private String title; - private Revision destination; - private String reason; - private String closedBy; - private Revision source; - private String state; - private String createdOn; - private String updatedOn; - private String mergeCommit; - private String id; - private Author author; - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class Response { - private int pageLength; - private List values; - private int page; - private int size; - private String next; - - @JsonProperty("pagelen") - public int getPageLength() { - return pageLength; - } - @JsonProperty("pagelen") - public void setPageLength(int pageLength) { - this.pageLength = pageLength; - } - public List getValues() { - return values; - } - public void setValues(List values) { - this.values = values; - } - public int getPage() { - return page; - } - public void setPage(int page) { - this.page = page; - } - public int getSize() { - return size; - } - public void setSize(int size) { - this.size = size; - } - public String getNext() { - return next; - } - public void setNext(String next) { - this.next = next; - } - } - - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class Revision { - private Repository repository; - private Branch branch; - private Commit commit; - - public Repository getRepository() { - return repository; - } - public void setRepository(Repository repository) { - this.repository = repository; - } - public Branch getBranch() { - return branch; - } - public void setBranch(Branch branch) { - this.branch = branch; - } - public Commit getCommit() { - return commit; - } - public void setCommit(Commit commit) { - this.commit = commit; - } - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class Repository { - private String fullName; - private String name; - private String ownerName; - private String repositoryName; - - @JsonProperty("full_name") - public String getFullName() { - return fullName; - } - @JsonProperty("full_name") - public void setFullName(String fullName) { - // Also extract owner- and reponame - if (fullName != null) { - this.ownerName = fullName.split("/")[0]; - this.repositoryName = fullName.split("/")[1]; - } - this.fullName = fullName; - } - public String getName() { - return name; - } - public void setName(String name) { - this.name = name; - } - public String getOwnerName() { - return ownerName; - } - public String getRepositoryName() { - return repositoryName; - } - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class Branch { - private String name; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class Commit { - private String hash; - - public String getHash() { - return hash; - } - - public void setHash(String hash) { - this.hash = hash; - } - } - - // Was: Approval - @JsonIgnoreProperties(ignoreUnknown = true) - public static class Participant { - private String role; - private Boolean approved; - - public String getRole() { - return role; - } - public void setRole(String role) { - this.role = role; - } - public Boolean getApproved() { - return approved; - } - public void setApproved(Boolean approved) { - this.approved = approved; - } - } - - // https://confluence.atlassian.com/bitbucket/pullrequests-resource-1-0-296095210.html#pullrequestsResource1.0-POSTanewcomment - @JsonIgnoreProperties(ignoreUnknown = true) - public static class Comment implements Comparable { - private Integer id; - private String filename; - private String content; - - @Override - public int compareTo(Comment target) { - if (target == null){ - return -1; - } else if (this.getId() > target.getId()) { - return 1; - } else if (this.getId().equals(target.getId())) { - return 0; - } else { - return -1; - } - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - final Comment comment = (Comment) o; - - return getId() != null ? getId().equals(comment.getId()) : comment.getId() == null; - } - - @Override - public int hashCode() { - return getId() != null ? getId().hashCode() : 0; - } - - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - public String getFilename() { - return filename; - } - - public void setFilename(String filename) { - this.filename = filename; - } - - public String getContent() { - return content; - } - - public void setContent(Object content) { - if (content instanceof String) { - this.content = (String)content; - } else if (content instanceof Map){ - this.content = (String)((Map)content).get("raw"); - } - return; - } - - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class Author { - private String username; - private String display_name; - public static final String COMBINED_NAME = "%s <@%s>"; - - public String getUsername() { - return username; - } - public void setUsername(String username) { - this.username = username; - } - - @JsonProperty("display_name") - public String getDisplayName() { - return display_name; - } - - @JsonProperty("display_name") - public void setDisplayName(String display_name) { - this.display_name = display_name; - } - public String getCombinedUsername() { - return String.format(COMBINED_NAME, this.getDisplayName(), this.getUsername()); - } - } - - //-------------------- only getters and setters follow ----------------- - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - @JsonProperty("close_source_branch") - public Boolean getCloseSourceBranch() { - return closeSourceBranch; - } - - @JsonProperty("close_source_branch") - public void setCloseSourceBranch(Boolean closeSourceBranch) { - this.closeSourceBranch = closeSourceBranch; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public Revision getDestination() { - return destination; - } - - public void setDestination(Revision destination) { - this.destination = destination; - } - - public String getReason() { - return reason; - } - - public void setReason(String reason) { - this.reason = reason; - } - - @JsonProperty("closed_by") - public String getClosedBy() { - return closedBy; - } - - @JsonProperty("closed_by") - public void setClosedBy(String closedBy) { - this.closedBy = closedBy; - } - - public Revision getSource() { - return source; - } - - public void setSource(Revision source) { - this.source = source; - } - - public String getState() { - return state; - } - - public void setState(String state) { - this.state = state; - } - - @JsonProperty("created_on") - public String getCreatedOn() { - return createdOn; - } - - @JsonProperty("created_on") - public void setCreatedOn(String createdOn) { - this.createdOn = createdOn; - } - - @JsonProperty("updated_on") - public String getUpdatedOn() { - return updatedOn; - } - - @JsonProperty("updated_on") - public void setUpdatedOn(String updatedOn) { - this.updatedOn = updatedOn; - } - - @JsonProperty("merge_commit") - public String getMergeCommit() { - return mergeCommit; - } - - @JsonProperty("merge_commit") - public void setMergeCommit(String mergeCommit) { - this.mergeCommit = mergeCommit; - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public Author getAuthor() { - return this.author; - } - - public void setAutohor(Author author) { - this.author = author; - } - -} diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketAdditionalParameterEnvironmentContributor.java b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketAdditionalParameterEnvironmentContributor.java new file mode 100644 index 0000000..2e9d8bc --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketAdditionalParameterEnvironmentContributor.java @@ -0,0 +1,39 @@ +package org.jenkinsci.plugins.bbprb; + +import hudson.EnvVars; +import hudson.Extension; +import hudson.model.*; + +import java.io.IOException; + +@Extension +public class BitbucketAdditionalParameterEnvironmentContributor + extends EnvironmentContributor { + @Override + public void buildEnvironmentFor(Run run, EnvVars envVars, + TaskListener taskListener) + throws IOException, InterruptedException { + + BitbucketCause cause = (BitbucketCause)run.getCause(BitbucketCause.class); + if (cause == null) { + return; + } + + putEnvVar(envVars, "destinationRepository", + cause.getDestinationRepository()); + putEnvVar(envVars, "pullRequestAuthor", cause.getPullRequestAuthor()); + putEnvVar(envVars, "pullRequestId", cause.getPullRequestId()); + putEnvVar(envVars, "pullRequestTitle", cause.getPullRequestTitle()); + putEnvVar(envVars, "sourceBranch", cause.getSourceBranch()); + putEnvVar(envVars, "sourceRepository", cause.getSourceRepository()); + putEnvVar(envVars, "targetBranch", cause.getTargetBranch()); + } + + private static void putEnvVar(EnvVars envs, String name, String value) { + envs.put(name, getString(value, "")); + } + + private static String getString(String actual, String d) { + return actual == null ? d : actual; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildListener.java b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildListener.java new file mode 100644 index 0000000..efcc13a --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildListener.java @@ -0,0 +1,69 @@ +package org.jenkinsci.plugins.bbprb; + +import hudson.Extension; +import hudson.model.AbstractBuild; +import hudson.model.Job; +import hudson.model.TaskListener; +import hudson.model.listeners.RunListener; +import hudson.triggers.Trigger; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import hudson.model.Result; + +import org.jenkinsci.plugins.bbprb.bitbucket.BuildState; + +@Extension +public class BitbucketBuildListener extends RunListener> { + + @Override + public void onStarted(AbstractBuild build, TaskListener listener) { + BitbucketCause cause = build.getCause(BitbucketCause.class); + if (cause == null) { + return; + } + + BitbucketBuildTrigger trigger = extractTrigger(build); + if (trigger == null) { + return; + } + + LOGGER.log(Level.FINE, "Started by BitbucketBuildTrigger"); + trigger.setPRState(cause, BuildState.INPROGRESS, build.getUrl()); + try { + build.setDescription( + build.getCause(BitbucketCause.class).getShortDescription()); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Could not set build description: {0}", + e.getMessage()); + } + } + + @Override + public void onCompleted(AbstractBuild build, TaskListener listener) { + BitbucketBuildTrigger trigger = extractTrigger(build); + if (trigger != null) { + LOGGER.log(Level.FINE, "Completed after BitbucketBuildTrigger"); + Result result = build.getResult(); + BuildState state = (result == Result.SUCCESS) ? BuildState.SUCCESSFUL + : BuildState.FAILED; + BitbucketCause cause = build.getCause(BitbucketCause.class); + trigger.setPRState(cause, state, build.getUrl()); + } + } + + private static BitbucketBuildTrigger + extractTrigger(AbstractBuild build) { + BitbucketBuildTrigger trigger = + build.getProject().getTrigger(BitbucketBuildTrigger.class); + + if ((trigger != null) && (trigger instanceof BitbucketBuildTrigger)) { + return trigger; + } else { + return null; + } + } + + private static final Logger LOGGER = + Logger.getLogger(BitbucketBuildListener.class.getName()); +} diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger.java b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger.java new file mode 100644 index 0000000..b0c6ece --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger.java @@ -0,0 +1,303 @@ +package org.jenkinsci.plugins.bbprb; + +import antlr.ANTLRException; +import com.cloudbees.plugins.credentials.common.StandardListBoxModel; +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials; +import com.cloudbees.plugins.credentials.CredentialsMatchers; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; +import hudson.Extension; +import hudson.model.AbstractProject; +import hudson.model.Cause; +import hudson.model.Executor; +import hudson.model.Item; +import hudson.model.ParameterDefinition; +import hudson.model.ParametersAction; +import hudson.model.ParametersDefinitionProperty; +import hudson.model.ParameterValue; +import hudson.model.queue.QueueTaskFuture; +import hudson.model.Queue; +import hudson.model.Result; +import hudson.model.Run; +import hudson.plugins.git.RevisionParameterAction; +import hudson.security.ACL; +import hudson.triggers.Trigger; +import hudson.triggers.TriggerDescriptor; +import hudson.util.ListBoxModel; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.Map; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import jenkins.model.Jenkins; +import jenkins.model.ParameterizedJobMixIn; +import net.sf.json.JSONObject; +import org.acegisecurity.context.SecurityContext; +import org.acegisecurity.context.SecurityContextHolder; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.Symbol; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.StaplerRequest; +import static com.cloudbees.plugins.credentials.CredentialsMatchers.instanceOf; + +import org.jenkinsci.plugins.bbprb.bitbucket.ApiClient; +import org.jenkinsci.plugins.bbprb.bitbucket.BuildState; + +public class BitbucketBuildTrigger extends Trigger> { + private final String ciKey; + private final String ciName; + private final String credentialsId; + private final String destinationRepository; + private final boolean cancelOutdatedJobs; + private final boolean checkDestinationCommit; + + // XXX: This is for Jelly. + // https://wiki.jenkins.io/display/JENKINS/Basic+guide+to+Jelly+usage+in+Jenkins + public String getCiKey() { + return this.ciKey; + } + public String getCiName() { + return this.ciName; + } + public String getCredentialsId() { + return this.credentialsId; + } + public String getDestinationRepository() { + return this.destinationRepository; + } + public boolean getCancelOutdatedJobs() { + return this.cancelOutdatedJobs; + } + public boolean getCheckDestinationCommit() { + return this.checkDestinationCommit; + } + + private transient ApiClient apiClient; + + public static final BitbucketBuildTriggerDescriptor descriptor = + new BitbucketBuildTriggerDescriptor(); + + @DataBoundConstructor + public BitbucketBuildTrigger(String credentialsId, + String destinationRepository, String ciKey, + String ciName, boolean checkDestinationCommit, + boolean cancelOutdatedJobs) + throws ANTLRException { + super(); + this.apiClient = null; + this.cancelOutdatedJobs = cancelOutdatedJobs; + this.checkDestinationCommit = checkDestinationCommit; + this.ciKey = ciKey; + this.ciName = ciName; + this.credentialsId = credentialsId; + this.destinationRepository = destinationRepository; + } + + @Override + public void start(AbstractProject project, boolean newInstance) { + logger.log(Level.FINE, "Started for `{0}`", project.getFullName()); + + super.start(project, newInstance); + + if (credentialsId != null && !credentialsId.isEmpty()) { + logger.log(Level.FINE, "Looking up credentials `{0}`", + this.credentialsId); + List all = + CredentialsProvider.lookupCredentials( + UsernamePasswordCredentials.class, (Item)null, ACL.SYSTEM, + URIRequirementBuilder.fromUri("https://bitbucket.org").build()); + UsernamePasswordCredentials creds = CredentialsMatchers.firstOrNull( + all, CredentialsMatchers.withId(this.credentialsId)); + if (creds != null) { + logger.log(Level.INFO, "Creating Bitbucket API client"); + this.apiClient = new ApiClient( + creds.getUsername(), creds.getPassword().getPlainText(), + this.destinationRepository, this.ciKey, this.ciName); + } else { + logger.log(Level.SEVERE, "Credentials `{0}` not found", + this.credentialsId); + } + } else { + logger.log(Level.WARNING, "Missing Bitbucket API credentials"); + } + } + + public void setPRState(BitbucketCause cause, BuildState state, String path) { + if (this.apiClient != null) { + logger.log(Level.INFO, "Setting status of PR #{0} to {1} for {2}", + new Object[] {cause.getPullRequestId(), state, + cause.getDestinationRepository()}); + this.apiClient.setBuildStatus(cause.getSourceCommitHash(), state, + getInstance().getRootUrl() + path, null, + this.job.getFullName()); + } else { + logger.log(Level.INFO, + "Will not set Bitbucket PR build status (not configured)"); + } + } + + private void startJob(BitbucketCause cause) { + if (this.cancelOutdatedJobs) { + SecurityContext orig = ACL.impersonate(ACL.SYSTEM); + cancelPreviousJobsInQueueThatMatch(cause); + abortRunningJobsThatMatch(cause); + SecurityContextHolder.setContext(orig); + } + + setPRState(cause, BuildState.INPROGRESS, this.job.getUrl()); + + this.job.scheduleBuild2( + 0, cause, new ParametersAction(this.getDefaultParameters()), + new RevisionParameterAction(cause.getSourceCommitHash())); + } + + private void + cancelPreviousJobsInQueueThatMatch(@Nonnull BitbucketCause cause) { + logger.log(Level.FINE, "Looking for queued jobs that match PR #{0}", + cause.getPullRequestId()); + Queue queue = getInstance().getQueue(); + + for (Queue.Item item : queue.getItems()) { + if (hasCauseFromTheSamePullRequest(item.getCauses(), cause)) { + logger.fine("Canceling item in queue: " + item); + queue.cancel(item); + } + } + } + + private Jenkins getInstance() { + final Jenkins instance = Jenkins.getInstance(); + if (instance == null) { + throw new IllegalStateException("Jenkins instance is NULL!"); + } + return instance; + } + + private void + abortRunningJobsThatMatch(@Nonnull BitbucketCause bitbucketCause) { + logger.log(Level.FINE, "Looking for running jobs that match PR #{0}", + bitbucketCause.getPullRequestId()); + for (Object o : job.getBuilds()) { + if (o instanceof Run) { + Run build = (Run)o; + if (build.isBuilding() && + hasCauseFromTheSamePullRequest(build.getCauses(), bitbucketCause)) { + logger.fine("Aborting build: " + build + " since PR is outdated"); + setBuildDescription(build); + final Executor executor = build.getExecutor(); + if (executor == null) { + throw new IllegalStateException("Executor can't be NULL"); + } + executor.interrupt(Result.ABORTED); + } + } + } + } + + private void setBuildDescription(final Run build) { + try { + build.setDescription( + "Aborting build by `Bitbucket Pullrequest Builder Plugin`: " + build + + " since PR is outdated"); + } catch (IOException e) { + logger.warning("Could not set build description: " + e.getMessage()); + } + } + + private boolean + hasCauseFromTheSamePullRequest(@Nullable List causes, + @Nullable BitbucketCause pullRequestCause) { + if (causes != null && pullRequestCause != null) { + for (Cause cause : causes) { + if (cause instanceof BitbucketCause) { + BitbucketCause sc = (BitbucketCause)cause; + if (StringUtils.equals(sc.getPullRequestId(), + pullRequestCause.getPullRequestId()) && + StringUtils.equals(sc.getSourceRepository(), + pullRequestCause.getSourceRepository())) { + return true; + } + } + } + } + return false; + } + + private ArrayList getDefaultParameters() { + Map values = new HashMap(); + ParametersDefinitionProperty definitionProperty = + this.job.getProperty(ParametersDefinitionProperty.class); + + if (definitionProperty != null) { + for (ParameterDefinition definition : + definitionProperty.getParameterDefinitions()) { + values.put(definition.getName(), definition.getDefaultParameterValue()); + } + } + return new ArrayList(values.values()); + } + + public void handlePR(JSONObject pr) { + JSONObject src = pr.getJSONObject("source"); + JSONObject dst = pr.getJSONObject("destination"); + String dstRepository = + dst.getJSONObject("repository").getString("full_name"); + BitbucketCause cause = new BitbucketCause( + src.getJSONObject("branch").getString("name"), + dst.getJSONObject("branch").getString("name"), + src.getJSONObject("repository").getString("full_name"), + pr.getString("id"), // FIXME: it is integer + dstRepository, pr.getString("title"), + src.getJSONObject("commit").getString("hash"), + dst.getJSONObject("commit").getString("hash"), + pr.getJSONObject("author").getString("username")); + if (!dstRepository.equals(this.destinationRepository)) { + logger.log(Level.FINE, + "Job `{0}`: repository `{1}` does not match `{2}`. Skipping.", + new Object[] {this.job.getFullName(), dstRepository, + this.destinationRepository}); + return; + } + startJob(cause); + } + + @Extension + @Symbol("bbprb") + public static final class BitbucketBuildTriggerDescriptor + extends TriggerDescriptor { + public BitbucketBuildTriggerDescriptor() { + load(); + } + + @Override + public boolean isApplicable(Item item) { + return item instanceof AbstractProject; + } + + @Override + public String getDisplayName() { + return "Bitbucket Pull Requests Builder"; + } + + @Override + public boolean configure(StaplerRequest req, JSONObject json) + throws FormException { + save(); + return super.configure(req, json); + } + + public ListBoxModel doFillCredentialsIdItems() { + return new StandardListBoxModel().withEmptySelection().withMatching( + instanceOf(UsernamePasswordCredentials.class), + CredentialsProvider.lookupCredentials( + StandardUsernamePasswordCredentials.class)); + } + } + private static final Logger logger = + Logger.getLogger(BitbucketBuildTrigger.class.getName()); +} diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketCause.java b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketCause.java new file mode 100644 index 0000000..a4a20a6 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketCause.java @@ -0,0 +1,80 @@ +package org.jenkinsci.plugins.bbprb; + +import hudson.model.Cause; + +/** + * Created by nishio + */ +public class BitbucketCause extends Cause { + private final String sourceBranch; + private final String targetBranch; + private final String sourceRepository; + private final String pullRequestId; + private final String destinationRepository; + private final String pullRequestTitle; + private final String sourceCommitHash; + private final String destinationCommitHash; + private final String pullRequestAuthor; + public static final String BITBUCKET_URL = "https://bitbucket.org/"; + + public BitbucketCause(String sourceBranch, String targetBranch, + String sourceRepository, String pullRequestId, + String destinationRepository, String pullRequestTitle, + String sourceCommitHash, String destinationCommitHash, + String pullRequestAuthor) { + this.sourceBranch = sourceBranch; + this.targetBranch = targetBranch; + this.sourceRepository = sourceRepository; + this.pullRequestId = pullRequestId; + this.destinationRepository = destinationRepository; + this.pullRequestTitle = pullRequestTitle; + this.sourceCommitHash = sourceCommitHash; + this.destinationCommitHash = destinationCommitHash; + this.pullRequestAuthor = pullRequestAuthor; + } + + public String getSourceBranch() { + return sourceBranch; + } + public String getTargetBranch() { + return targetBranch; + } + + public String getSourceRepository() { + return sourceRepository; + } + + public String getPullRequestId() { + return pullRequestId; + } + + public String getDestinationRepository() { + return destinationRepository; + } + + public String getPullRequestTitle() { + return pullRequestTitle; + } + + public String getSourceCommitHash() { + return sourceCommitHash; + } + + public String getDestinationCommitHash() { + return destinationCommitHash; + } + + @Override + public String getShortDescription() { + String description = + "#" + this.getPullRequestId() + " " + + this.getPullRequestTitle() + ""; + return description; + } + + public String getPullRequestAuthor() { + return this.pullRequestAuthor; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketHookReceiver.java b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketHookReceiver.java new file mode 100644 index 0000000..18b2688 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketHookReceiver.java @@ -0,0 +1,140 @@ +package org.jenkinsci.plugins.bbprb; + +import hudson.Extension; +import hudson.model.UnprotectedRootAction; +import hudson.security.ACL; +import hudson.triggers.Trigger; +import hudson.triggers.TriggerDescriptor; +import java.io.IOException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.model.Jenkins; +import jenkins.model.ParameterizedJobMixIn.ParameterizedJob; +import net.sf.json.JSONException; +import net.sf.json.JSONObject; +import org.acegisecurity.context.SecurityContext; +import org.acegisecurity.context.SecurityContextHolder; +import org.apache.commons.io.IOUtils; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +@Extension +public class BitbucketHookReceiver implements UnprotectedRootAction { + + private static final String BITBUCKET_HOOK_URL = "bbprb-hook"; + private static final String BITBUCKET_UA = "Bitbucket-Webhooks/2.0"; + + public void doIndex(StaplerRequest req, StaplerResponse resp) + throws IOException { + + String userAgent = req.getHeader("user-agent"); + if (!BITBUCKET_UA.equals(userAgent)) { + LOGGER.log(Level.WARNING, "Bad user agent: `{0}`, expected `{1}`", + new Object[] {userAgent, BITBUCKET_UA}); + resp.setStatus(StaplerResponse.SC_BAD_REQUEST); + return; + } + + String uri = req.getRequestURI(); + if (!uri.contains("/" + BITBUCKET_HOOK_URL + "/")) { + LOGGER.log(Level.WARNING, + "BitBucket hook URI does not contain `/{0}/`: `{1}`", + new Object[] {BITBUCKET_HOOK_URL, uri}); + resp.setStatus(StaplerResponse.SC_NOT_FOUND); + return; + } + + String event = req.getHeader("x-event-key"); + if (event == null) { + LOGGER.log(Level.WARNING, "Missing the `x-event-key` header"); + resp.setStatus(StaplerResponse.SC_BAD_REQUEST); + return; + } + + String body = IOUtils.toString(req.getInputStream()); + if (body.isEmpty()) { + LOGGER.log(Level.WARNING, "Received empty request body"); + resp.setStatus(StaplerResponse.SC_BAD_REQUEST); + return; + } + + String contentType = req.getContentType(); + if (contentType != null && + contentType.startsWith("application/x-www-form-urlencoded")) { + body = URLDecoder.decode(body, "UTF-8"); + } + if (body.startsWith("payload=")) { + body = body.substring(8); + } + + LOGGER.log(Level.FINE, + "Received commit hook notification, key: `{0}`, body: `{1}`", + new Object[] {event, body}); + + try { + JSONObject payload = JSONObject.fromObject(body); + if (event.startsWith("pullrequest:")) { + JSONObject pr = payload.getJSONObject("pullrequest"); + String state = pr.getString("state"); + if (!"OPEN".equals(state)) { + LOGGER.log( + Level.INFO, "Ignoring closed PR ({0}): #{1} {2}", + new Object[] {state, pr.getInt("id"), pr.getString("title")}); + return; + } + for (BitbucketBuildTrigger trigger : getBitbucketTriggers()) { + trigger.handlePR(pr); + } + return; + } + } catch (JSONException e) { + LOGGER.log(Level.WARNING, e.getMessage()); + resp.setStatus(StaplerResponse.SC_BAD_REQUEST); + return; + } + } + + private static List getBitbucketTriggers() { + List bbtriggers = new ArrayList<>(); + + SecurityContext orig = ACL.impersonate(ACL.SYSTEM); + List jobs = + Jenkins.getInstance().getAllItems(ParameterizedJob.class); + SecurityContextHolder.setContext(orig); + + for (ParameterizedJob job : jobs) { + String jobName = job.getFullName(); + LOGGER.log(Level.FINER, "Found job: `{0}`", jobName); + + Map> triggers = job.getTriggers(); + + for (Trigger trigger : triggers.values()) { + if (trigger instanceof BitbucketBuildTrigger) { + LOGGER.log(Level.FINE, "Will consider job: `{0}`", jobName); + bbtriggers.add((BitbucketBuildTrigger)trigger); + } + } + } + + return bbtriggers; + } + + private static final Logger LOGGER = + Logger.getLogger(BitbucketHookReceiver.class.getName()); + + public String getIconFileName() { + return null; + } + + public String getDisplayName() { + return null; + } + + public String getUrlName() { + return BITBUCKET_HOOK_URL; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/ApiClient.java b/src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/ApiClient.java new file mode 100644 index 0000000..80ae3aa --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/ApiClient.java @@ -0,0 +1,246 @@ +package org.jenkinsci.plugins.bbprb.bitbucket; + +import org.apache.commons.httpclient.*; +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.DeleteMethod; +import org.apache.commons.httpclient.params.HttpClientParams; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.type.TypeFactory; +import org.codehaus.jackson.type.JavaType; +import org.codehaus.jackson.type.TypeReference; + +import java.io.IOException; +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 hudson.ProxyConfiguration; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.httpclient.methods.PutMethod; +import org.apache.commons.httpclient.util.EncodingUtil; + +/** + * Created by nishio + */ +public class ApiClient { + private static final Logger logger = + Logger.getLogger(ApiClient.class.getName()); + private static final String V1_API_BASE_URL = + "https://bitbucket.org/api/1.0/repositories/"; + private static final String V2_API_BASE_URL = + "https://bitbucket.org/api/2.0/repositories/"; + private static final String COMPUTED_KEY_FORMAT = "%s-%s"; + private String repository; + private Credentials credentials; + private String key; + private String name; + private HttpClientFactory factory; + + public static final byte MAX_KEY_SIZE_BB_API = 40; + + public static class HttpClientFactory { + public static final HttpClientFactory INSTANCE = new HttpClientFactory(); + private static final int DEFAULT_TIMEOUT = 60000; + + public HttpClient getInstanceHttpClient() { + HttpClient client = new HttpClient(); + + HttpClientParams params = client.getParams(); + params.setConnectionManagerTimeout(DEFAULT_TIMEOUT); + params.setSoTimeout(DEFAULT_TIMEOUT); + + if (Jenkins.getInstance() == null) + return client; + + ProxyConfiguration proxy = getInstance().proxy; + if (proxy == null) + return client; + + logger.log(Level.FINE, "Jenkins proxy: {0}:{1}", + new Object[] {proxy.name, proxy.port}); + client.getHostConfiguration().setProxy(proxy.name, proxy.port); + String username = proxy.getUserName(); + String password = proxy.getPassword(); + + // Consider it to be passed if username specified. Sufficient? + if (username != null && !"".equals(username.trim())) { + logger.log(Level.FINE, "Using proxy authentication (user={0})", + username); + client.getState().setProxyCredentials( + AuthScope.ANY, new UsernamePasswordCredentials(username, password)); + } + + return client; + } + + private Jenkins getInstance() { + final Jenkins instance = Jenkins.getInstance(); + if (instance == null) { + throw new IllegalStateException("Jenkins instance is NULL!"); + } + return instance; + } + } + + public ApiClient(String username, + String password, + String repository, String key, + String name) { + this.credentials = new UsernamePasswordCredentials(username, password); + this.repository = repository; + this.key = key; + this.name = name; + this.factory = HttpClientFactory.INSTANCE; + } + + public String getName() { + return this.name; + } + + private static MessageDigest SHA1 = null; + + /** + * Retrun + * @param keyExPart + * @return key parameter for call BitBucket API + */ + private String computeAPIKey(String keyExPart) { + String computedKey = + String.format(COMPUTED_KEY_FORMAT, this.key, keyExPart); + + if (computedKey.length() > MAX_KEY_SIZE_BB_API) { + try { + if (SHA1 == null) + SHA1 = MessageDigest.getInstance("SHA1"); + return new String( + Hex.encodeHex(SHA1.digest(computedKey.getBytes("UTF-8")))); + } catch (NoSuchAlgorithmException e) { + logger.log(Level.WARNING, "Failed to create hash provider", e); + } catch (UnsupportedEncodingException e) { + logger.log(Level.WARNING, "Failed to create hash provider", e); + } + } + return (computedKey.length() <= MAX_KEY_SIZE_BB_API) + ? computedKey + : computedKey.substring(0, MAX_KEY_SIZE_BB_API); + } + + public String buildStatusKey(String bsKey) { + return this.computeAPIKey(bsKey); + } + + public boolean hasBuildStatus(String revision, String keyEx) { + String url = v2("/commit/" + revision + "/statuses/build/" + + this.computeAPIKey(keyEx)); + String reqBody = get(url); + return reqBody != null && reqBody.contains("\"state\""); + } + + public void setBuildStatus(String revision, BuildState state, String buildUrl, + String comment, String keyEx) { + String url = v2("/commit/" + revision + "/statuses/build"); + String computedKey = this.computeAPIKey(keyEx); + NameValuePair[] data = new NameValuePair[] { + new NameValuePair("description", comment), + new NameValuePair("key", computedKey), + new NameValuePair("name", this.name), + new NameValuePair("state", state.toString()), + new NameValuePair("url", buildUrl), + }; + logger.log(Level.FINE, + "POST state {0} to {1} with key {2} with response {3}", + new Object[] {state, url, computedKey, post(url, data)}); + } + + public void deletePullRequestApproval(String pullRequestId) { + delete(v2("/pullrequests/" + pullRequestId + "/approve")); + } + + public void deletePullRequestComment(String pullRequestId, String commentId) { + delete(v1("/pullrequests/" + pullRequestId + "/comments/" + commentId)); + } + + public void updatePullRequestComment(String pullRequestId, String content, + String commentId) { + NameValuePair[] data = new NameValuePair[] { + new NameValuePair("content", content), + }; + put(v1("/pullrequests/" + pullRequestId + "/comments/" + commentId), data); + } + + private HttpClient getHttpClient() { + return this.factory.getInstanceHttpClient(); + } + + private String v1(String path) { + return V1_API_BASE_URL + this.repository + path; + } + + private String v2(String path) { + return V2_API_BASE_URL + this.repository + path; + } + + private String get(String path) { + return send(new GetMethod(path)); + } + + private String post(String path, NameValuePair[] data) { + PostMethod req = new PostMethod(path); + req.setRequestBody(data); + req.getParams().setContentCharset("utf-8"); + return send(req); + } + + private void delete(String path) { + send(new DeleteMethod(path)); + } + + private void put(String path, NameValuePair[] data) { + PutMethod req = new PutMethod(path); + req.setRequestBody(EncodingUtil.formUrlEncode(data, "utf-8")); + req.getParams().setContentCharset("utf-8"); + send(req); + } + + private String send(HttpMethodBase req) { + HttpClient client = getHttpClient(); + client.getState().setCredentials(AuthScope.ANY, credentials); + client.getParams().setAuthenticationPreemptive(true); + try { + int statusCode = client.executeMethod(req); + if (statusCode != HttpStatus.SC_OK) { + logger.log(Level.WARNING, "Response status: " + req.getStatusLine() + + " URI: " + req.getURI()); + } else { + return req.getResponseBodyAsString(); + } + } catch (HttpException e) { + logger.log(Level.WARNING, "Failed to send request.", e); + } catch (IOException e) { + logger.log(Level.WARNING, "Failed to send request.", e); + } finally { + req.releaseConnection(); + } + return null; + } + + private R parse(String response, Class cls) throws IOException { + return new ObjectMapper().readValue(response, cls); + } + private R parse(String response, JavaType type) throws IOException { + return new ObjectMapper().readValue(response, type); + } + private R parse(String response, TypeReference ref) + throws IOException { + return new ObjectMapper().readValue(response, ref); + } +} diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/BuildState.java b/src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/BuildState.java new file mode 100644 index 0000000..8fe1cdd --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/BuildState.java @@ -0,0 +1,9 @@ +package org.jenkinsci.plugins.bbprb.bitbucket; + +/** + * Valid build states for a pull request + * + * @see + * "https://confluence.atlassian.com/bitbucket/buildstatus-resource-779295267.html" + */ +public enum BuildState { FAILED, INPROGRESS, SUCCESSFUL } diff --git a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/config.jelly b/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/config.jelly deleted file mode 100644 index 1d7c4e4..0000000 --- a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/config.jelly +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-branchesFilter.html b/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-branchesFilter.html deleted file mode 100644 index 30e5f5e..0000000 --- a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-branchesFilter.html +++ /dev/null @@ -1,19 +0,0 @@ -
- Filter option in custom format. Default value is empty or any.
- Available formats: -
    -
  • any pull requests applied for this project: any, * or empty string
  • -
  • filtered by destination branch: my-branch or more complex reg-ex filter r:^master (must be started with r: and case insensitive match).
  • -
  • filtered by source and destination branches: s:source-branch d:dest-branch
  • -
  • filtered by source and destination branches with regex: s:r:^feature d:r:master$
  • -
  • filtered by many destination/source branches: s:one s:two s:three d:master d:r:master$
  • -
  • filtered by many sources branches: s:one s:two s:r:^three d:
  • -
-

- When you using format with source branch filter s or destination filter d, you must specify great than one source and destination filter, eg s:1 s:2 s:... d:.
- Any sources and any destinations for pull request: -

    -
  • filter string: *
  • -
  • filter string: s: d:
  • -
-
diff --git a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-branchesFilterBySCMIncludes.html b/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-branchesFilterBySCMIncludes.html deleted file mode 100644 index cc9d47c..0000000 --- a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-branchesFilterBySCMIncludes.html +++ /dev/null @@ -1,7 +0,0 @@ -Uses the Git SCM option "Branches to build" option as the value for -"BranchesFilter". If the "BranchesFilter" field itself has any content, -it will be ignored. -
-If the "Branches to build" option has values -"*/master */feature-master */build-with-jenkins", then "BranchesFilter" -field will have value "d:master d:feature-master d:build-with-jenkins". diff --git a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-cancelOutdatedJobs.html b/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-cancelOutdatedJobs.html deleted file mode 100644 index c03651c..0000000 --- a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-cancelOutdatedJobs.html +++ /dev/null @@ -1 +0,0 @@ -If you make a new commit into your PR and there is already running job on that PR, this option will cancel such a outdated job and allows to run only one job at given PR with the newest commit. \ No newline at end of file diff --git a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-ciKey.html b/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-ciKey.html deleted file mode 100644 index 97696b6..0000000 --- a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-ciKey.html +++ /dev/null @@ -1,4 +0,0 @@ -The identifier needs to be unique among your Jenkins jobs related to this repo. -This identifier is used to decide whether a commit is already built by this job and to set status for a newly built commit. -If the value is changed rebuilds may occur and multiple statuses might show on an existing pull request. -The value is not shown for end users of Bitbucket. diff --git a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-ciName.html b/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-ciName.html deleted file mode 100644 index 32f248d..0000000 --- a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-ciName.html +++ /dev/null @@ -1 +0,0 @@ -This value is the name of the current job when showing build statuses for a pull request. diff --git a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-ciSkipPhrases.html b/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-ciSkipPhrases.html deleted file mode 100644 index f30f306..0000000 --- a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-ciSkipPhrases.html +++ /dev/null @@ -1,5 +0,0 @@ -A comma-separated list of strings to search the pull request title for. -
-e.g. If set to "trivial,[skiptest]", any PRs containing either "trivial" or -"[skiptest]" (case-insensitive) will not be built. - diff --git a/src/main/resources/index.jelly b/src/main/resources/index.jelly index 372d9f8..b295683 100644 --- a/src/main/resources/index.jelly +++ b/src/main/resources/index.jelly @@ -3,5 +3,5 @@ This view is used to render the installed plugins page. -->
- This plugin polls BitBucket to determine whether there are Pull Requests that should be built. + This plugin build Bitbucket pull requests via web-hooks
diff --git a/src/main/resources/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger/config.jelly b/src/main/resources/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger/config.jelly new file mode 100644 index 0000000..f61601a --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger/config.jelly @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger/help-cancelOutdatedJobs.html b/src/main/resources/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger/help-cancelOutdatedJobs.html new file mode 100644 index 0000000..c03651c --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger/help-cancelOutdatedJobs.html @@ -0,0 +1 @@ +If you make a new commit into your PR and there is already running job on that PR, this option will cancel such a outdated job and allows to run only one job at given PR with the newest commit. \ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger/help-ciKey.html b/src/main/resources/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger/help-ciKey.html new file mode 100644 index 0000000..97696b6 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger/help-ciKey.html @@ -0,0 +1,4 @@ +The identifier needs to be unique among your Jenkins jobs related to this repo. +This identifier is used to decide whether a commit is already built by this job and to set status for a newly built commit. +If the value is changed rebuilds may occur and multiple statuses might show on an existing pull request. +The value is not shown for end users of Bitbucket. diff --git a/src/main/resources/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger/help-ciName.html b/src/main/resources/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger/help-ciName.html new file mode 100644 index 0000000..32f248d --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger/help-ciName.html @@ -0,0 +1 @@ +This value is the name of the current job when showing build statuses for a pull request. diff --git a/src/test/java/BitbucketBuildFilterTest.java b/src/test/java/BitbucketBuildFilterTest.java deleted file mode 100644 index ad9892c..0000000 --- a/src/test/java/BitbucketBuildFilterTest.java +++ /dev/null @@ -1,308 +0,0 @@ - -import antlr.ANTLRException; -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketBuildFilter; -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketBuildTrigger; -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketCause; -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketPullRequestsBuilder; -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketRepository; -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.ApiClient; -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.Pullrequest; - -import java.util.Calendar; -import java.util.LinkedList; -import java.util.List; -import java.util.regex.Pattern; - -import jenkins.plugins.git.AbstractGitSCMSource; -import org.jvnet.hudson.test.JenkinsRule; -import org.jvnet.hudson.test.WithoutJenkins; - -import org.easymock.*; -import org.junit.Test; -import org.junit.Rule; -import static org.junit.Assert.*; - -/** - * Tests - */ -public class BitbucketBuildFilterTest { - - @Rule - public JenkinsRule jRule = new JenkinsRule(); - - @Test - @WithoutJenkins - public void mockTest() { - BitbucketCause cause = EasyMock.createMock(BitbucketCause.class); - EasyMock.expect(cause.getTargetBranch()).andReturn("mock").anyTimes(); - EasyMock.replay(cause); - for(Integer i : new Integer[] {1, 2, 3, 4, 5}) assertEquals("mock", cause.getTargetBranch()); - } - - @Test - @WithoutJenkins - public void anyFilter() { - BitbucketCause cause = EasyMock.createMock(BitbucketCause.class); - EasyMock.expect(cause.getTargetBranch()).andReturn("master").anyTimes(); - EasyMock.replay(cause); - - for(String f : new String[] {"", "*", "any"}) { - BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); - assertTrue(filter.approved(cause)); - } - - for(String f : new String[] {"foo", "bar", " baz "}) { - BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); - assertFalse(filter.approved(cause)); - } - } - - @Test - @WithoutJenkins - public void onlyDestinationFilter() { - BitbucketCause cause = EasyMock.createMock(BitbucketCause.class); - EasyMock.expect(cause.getTargetBranch()).andReturn("master-branch").anyTimes(); - EasyMock.replay(cause); - - for(String f : new String[] {"master-branch", "r:^master", "r:branch$", " master-branch "}) { - BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); - assertTrue(filter.approved(cause)); - } - - for(String f : new String[] {"develop", "feature-good-thing", "r:develop$"}) { - BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); - assertFalse(filter.approved(cause)); - } - } - - @Test - @WithoutJenkins - public void rxSourceDestCheck() { - for(String f : new String[] {"", "master", "r:master", "*"}) - assertFalse(Pattern.compile("(s:)|(d:)").matcher(f).find()); - - for(String f : new String[] {"s:master d:feature-master", "s:master d:r:^feature", "s:r:^master d:r:^feature"}) - assertTrue(Pattern.compile("(s:)|(d:)").matcher(f).find()); - } - - @Test - @WithoutJenkins - public void sourceAndDestFilter() { - BitbucketCause cause = EasyMock.createMock(BitbucketCause.class); - EasyMock.expect(cause.getTargetBranch()).andReturn("master").anyTimes(); - EasyMock.expect(cause.getSourceBranch()).andReturn("feature-for-master").anyTimes(); - EasyMock.replay(cause); - - for(String f : new String[] {"s:feature-for-master d:master", "s:r:^feature d:master", "s:feature-for-master d:r:^m", "s:r:^feature d:r:^master"}) { - BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); - assertTrue(filter.approved(cause)); - } - - for(String f : new String[] {"s:feature-for-master d:foo", "s:bar d:master", "s:foo d:bar"}) { - BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); - assertFalse(filter.approved(cause)); - } - } - - @Test - @WithoutJenkins - public void multipleSrcDestFilter() { - BitbucketCause cause = EasyMock.createMock(BitbucketCause.class); - EasyMock.expect(cause.getTargetBranch()).andReturn("master").anyTimes(); - EasyMock.expect(cause.getSourceBranch()).andReturn("feature-master").anyTimes(); - EasyMock.replay(cause); - - for(String f : new String[] {"s: d:", "s:r:^feature s:good-branch d:r:.*", "s:good-branch s:feature-master d:r:.*", "s: d:r:.*", "d:master d:foo d:bar s:"}) { - BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); - assertTrue(filter.approved(cause)); - } - - for(String f : new String[] {"d:ggg d:ooo d:333 s:feature-master", "s:111 s:222 s:333 d:master"}) { - BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); - assertFalse(filter.approved(cause)); - } - } - - @Test - @WithoutJenkins - public void sourceAndDestPartiallyFilter() { - BitbucketCause cause = EasyMock.createMock(BitbucketCause.class); - EasyMock.expect(cause.getTargetBranch()).andReturn("master").anyTimes(); - EasyMock.expect(cause.getSourceBranch()).andReturn("feature-master").anyTimes(); - EasyMock.replay(cause); - - for(String f : new String[] {"s:feature-master d:", "d:master s:"}) { - BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); - assertTrue(filter.approved(cause)); - } - - for(String f : new String[] {"s:feature-master", "d:master"}) { - BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); - assertFalse(filter.approved(cause)); - } - } - - @Test - @WithoutJenkins - public void authorFilter() { - BitbucketCause cause = EasyMock.createMock(BitbucketCause.class); - EasyMock.expect(cause.getTargetBranch()).andReturn("master").anyTimes(); - EasyMock.expect(cause.getSourceBranch()).andReturn("feature-master").anyTimes(); - EasyMock.expect(cause.getPullRequestAuthor()).andReturn("test").anyTimes(); - EasyMock.replay(cause); - - for(String f : new String[] {"a:test", "a:r:^test", "d: s: a:", "a:", "a:foo a:test"}) { - BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); - assertTrue(filter.approved(cause)); - } - - for(String f : new String[] {"s:feature-master", "d:master", "s:feature-master d: a:foo", "a:bar"}) { - BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); - assertFalse(filter.approved(cause)); - } - } - - @Test - @WithoutJenkins - public void emptyGitSCMFilter() { - BitbucketCause cause = EasyMock.createMock(BitbucketCause.class); - EasyMock.expect(cause.getTargetBranch()).andReturn("master").anyTimes(); - EasyMock.replay(cause); - - assertTrue(BitbucketBuildFilter.filterFromGitSCMSource(null, "").isEmpty()); - assertEquals("default", BitbucketBuildFilter.filterFromGitSCMSource(null, "default")); - - assertTrue(BitbucketBuildFilter.instanceByString( - BitbucketBuildFilter.filterFromGitSCMSource(null, "")).approved(cause) - ); - } - - @Test - @WithoutJenkins - public void fromGitSCMFilter() { - AbstractGitSCMSource git = EasyMock.createMock(AbstractGitSCMSource.class); - EasyMock.expect(git.getIncludes()) - .andReturn("").times(1) - .andReturn("").times(1) - .andReturn("*/master */feature-branch").times(1) - .andReturn("*/master").anyTimes(); - EasyMock.replay(git); - - assertTrue(git.getIncludes().isEmpty()); - assertEquals("", BitbucketBuildFilter.filterFromGitSCMSource(git, "")); - assertEquals("d:master d:feature-branch", BitbucketBuildFilter.filterFromGitSCMSource(git, "")); - assertEquals("d:master", BitbucketBuildFilter.filterFromGitSCMSource(git, "")); - } - - @Test - @WithoutJenkins - public void filterPRComments() throws ANTLRException { - BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); - EasyMock.expect(builder.getTrigger()).andReturn(null).anyTimes(); - EasyMock.replay(builder); - - List comments = new LinkedList(); - for(String commentContent : new String[] { - "check", - "", - "Hello from mock", - "Jenkins: test this please", - "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970]", - "check", - "", - "Hello from mock", - "Jenkins: test this please", - "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970]", - "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970 #jenkins-foo]", - "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970 #jenkins-foo #jenkins-bar]", - }) { - Pullrequest.Comment comment = EasyMock.createNiceMock(Pullrequest.Comment.class); - EasyMock.expect(comment.getContent()).andReturn(commentContent).anyTimes(); - EasyMock.expect(comment.getId()).andReturn(new java.sql.Timestamp(Calendar.getInstance().getTime().getTime()).getNanos()).anyTimes(); - EasyMock.replay(comment); - comments.add(comment); - } - - // Check twice - assertEquals("check", comments.get(0).getContent()); - assertEquals("check", comments.get(0).getContent()); - - assertEquals("Hello from mock", comments.get(2).getContent()); - - BitbucketRepository repo = new BitbucketRepository("", builder); - repo.init(EasyMock.createNiceMock(ApiClient.class)); - - List filteredComments = repo.filterPullRequestComments(comments); - - assertTrue(filteredComments.size() == 4); - assertEquals("Jenkins: test this please", filteredComments.get(filteredComments.size() - 1).getContent()); - } - - @Test - @WithoutJenkins - public void checkHashMyBuildTagTrue() { - BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); - EasyMock.expect(builder.getTrigger()).andReturn(null).anyTimes(); - EasyMock.replay(builder); - - IMockBuilder repoBuilder = EasyMock.partialMockBuilder(BitbucketRepository.class); - repoBuilder.addMockedMethod("getMyBuildTag"); - BitbucketRepository repo = repoBuilder.createMock(); - EasyMock.expect(repo.getMyBuildTag(EasyMock.anyString())).andReturn("#jenkins-902f259e962ff16100843123480a0970").anyTimes(); - EasyMock.replay(repo); - - List comments = new LinkedList(); - for(String commentContent : new String[] { - "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970]", - "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970 #jenkins-foo]", - "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970 #jenkins-foo #jenkins-bar]", - "TTP build flag ```[bid: #jenkins-902f259e962ff16100843123480a0970 #jenkins-foo #jenkins-bar]```", - }) { - Pullrequest.Comment comment = EasyMock.createNiceMock(Pullrequest.Comment.class); - EasyMock.expect(comment.getContent()).andReturn(commentContent).anyTimes(); - EasyMock.expect(comment.getId()).andReturn(new java.sql.Timestamp(Calendar.getInstance().getTime().getTime()).getNanos()).anyTimes(); - EasyMock.replay(comment); - comments.add(comment); - } - - String myBuildKey = "902f259e962ff16100843123480a0970"; - for(Pullrequest.Comment comment : comments) - assertTrue(repo.hasMyBuildTagInTTPComment(comment.getContent(), myBuildKey)); - } - - @Test - @WithoutJenkins - public void checkHashMyBuildTagFalse() { - BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); - EasyMock.expect(builder.getTrigger()).andReturn(null).anyTimes(); - EasyMock.replay(builder); - - IMockBuilder repoBuilder = EasyMock.partialMockBuilder(BitbucketRepository.class); - repoBuilder.addMockedMethod("getMyBuildTag"); - BitbucketRepository repo = repoBuilder.createMock(); - EasyMock.expect(repo.getMyBuildTag(EasyMock.anyString())).andReturn("#jenkins-902f259e962ff16100843123480a0970").anyTimes(); - EasyMock.replay(repo); - - List comments = new LinkedList(); - for(String commentContent : new String[] { - "check", - "", - "Hello from mock", - "Jenkins: test this please", - "TTP build flag [bid: #jenkins]", - "TTP build flag [bid: #jenkins-foo]", - "TTP build flag [bid: #jenkins-foo #jenkins-bar]", - "TTP build flag ```[bid: #jenkins-foo #jenkins-bar]```", - }) { - Pullrequest.Comment comment = EasyMock.createNiceMock(Pullrequest.Comment.class); - EasyMock.expect(comment.getContent()).andReturn(commentContent).anyTimes(); - EasyMock.expect(comment.getId()).andReturn(new java.sql.Timestamp(Calendar.getInstance().getTime().getTime()).getNanos()).anyTimes(); - EasyMock.replay(comment); - comments.add(comment); - } - - String myBuildKey = "902f259e962ff16100843123480a0970"; - for(Pullrequest.Comment comment : comments) - assertFalse(repo.hasMyBuildTagInTTPComment(comment.getContent(), myBuildKey)); - } -} diff --git a/src/test/java/BitbucketBuildRepositoryTest.java b/src/test/java/BitbucketBuildRepositoryTest.java deleted file mode 100644 index 42694a3..0000000 --- a/src/test/java/BitbucketBuildRepositoryTest.java +++ /dev/null @@ -1,354 +0,0 @@ - -import antlr.ANTLRException; - -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketBuildTrigger; -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketPullRequestsBuilder; -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketRepository; -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.ApiClient; - -import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.Pullrequest; -import com.cloudbees.plugins.credentials.CredentialsProvider; -import com.cloudbees.plugins.credentials.CredentialsScope; -import com.cloudbees.plugins.credentials.CredentialsStore; -import com.cloudbees.plugins.credentials.domains.Domain; -import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; - -import com.google.common.base.Function; -import com.google.common.collect.Collections2; - -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.logging.Logger; -import org.easymock.*; -import org.junit.Test; -import static org.junit.Assert.*; -import org.junit.Rule; -import org.junit.Assert; -import org.jvnet.hudson.test.JenkinsRule; - -import jenkins.model.Jenkins; - -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.httpclient.Credentials; -import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.HttpState; -import org.apache.commons.httpclient.UsernamePasswordCredentials; -import org.apache.commons.httpclient.auth.AuthScope; - - -interface ICredentialsInterceptor { - void assertCredentials(Credentials actual); -} - -/** - * Utility class for interceptor functionality - * @param - */ -class HttpClientInterceptor extends HttpClient { - private static final Logger logger = Logger.getLogger(HttpClientInterceptor.class.getName()); - - class CredentialsInterceptor extends HttpState { - private final T interceptor; - public CredentialsInterceptor(T interceptor) { this.interceptor = interceptor; } - - @Override - public synchronized void setCredentials(AuthScope authscope, Credentials credentials) { - logger.fine("Inject setCredentials"); - super.setCredentials(authscope, credentials); - this.interceptor.assertCredentials(credentials); - throw new AssertionError(); - } - } - - private final T interceptor; - public HttpClientInterceptor(T interceptor) { this.interceptor = interceptor; } - - @Override - public synchronized HttpState getState() { return new CredentialsInterceptor(this.interceptor); } -} - -/** - * Utility class for credentials assertion - * Used with - * @author maxvodo - */ -class AssertCredentials implements ICredentialsInterceptor { - private static final Logger logger = Logger.getLogger(AssertCredentials.class.getName()); - - private final Credentials expected; - public AssertCredentials(Credentials expected) { this.expected = expected; } - - public void assertCredentials(Credentials actual) { - logger.fine("Assert credential"); - if (actual == null) assertTrue(this.expected == null); - else assertTrue(this.expected != null); - - if (actual instanceof UsernamePasswordCredentials) { - UsernamePasswordCredentials actual_ = (UsernamePasswordCredentials)actual, - expected_ = (UsernamePasswordCredentials)this.expected; - assertNotNull(expected_); - Assert.assertArrayEquals(new Object[] { - actual_.getUserName(), actual_.getPassword() - }, new Object[] { - expected_.getUserName(), expected_.getPassword() - }); - } - } -} - -/** - * Tests - */ -public class BitbucketBuildRepositoryTest { - - @Rule - public JenkinsRule jRule = new JenkinsRule(); - - @Test - public void repositorySimpleUserPasswordTest() throws Exception { - BitbucketBuildTrigger trigger = new BitbucketBuildTrigger( - "", "@hourly", - "JenkinsCID", - "foo", - "bar", - "", "", - "", true, - "", "", "", - true, - true, - false, BitbucketRepository.DEFAULT_COMMENT_TRIGGER - ); - - BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); - EasyMock.expect(builder.getTrigger()).andReturn(trigger).anyTimes(); - EasyMock.replay(builder); - - ApiClient.HttpClientFactory httpFactory = EasyMock.createNiceMock(ApiClient.HttpClientFactory.class); - EasyMock.expect(httpFactory.getInstanceHttpClient()).andReturn( - new HttpClientInterceptor(new AssertCredentials(new UsernamePasswordCredentials("foo", "bar"))) - ).anyTimes(); - EasyMock.replay(httpFactory); - - BitbucketRepository repo = new BitbucketRepository("", builder); - repo.init(httpFactory); - - try { repo.postPullRequestApproval("prId"); } catch(Error e) { assertTrue(e instanceof AssertionError); } - } - - @Test - public void repositoryCtorWithTriggerTest() throws Exception { - BitbucketBuildTrigger trigger = new BitbucketBuildTrigger( - "", "@hourly", - "JenkinsCID", - "foo", - "bar", - "", "", - "", true, - "", "", "", - true, - true, - false, BitbucketRepository.DEFAULT_COMMENT_TRIGGER - ); - - BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); - EasyMock.expect(builder.getTrigger()).andReturn(trigger).anyTimes(); - EasyMock.replay(builder); - - CredentialsStore store = CredentialsProvider.lookupStores(Jenkins.getInstance()).iterator().next(); - assertNotNull(store); - store.addCredentials(Domain.global(), new UsernamePasswordCredentialsImpl( - CredentialsScope.GLOBAL, "JenkinsCID", "description", "username", "password" - )); - - ApiClient.HttpClientFactory httpFactory = EasyMock.createNiceMock(ApiClient.HttpClientFactory.class); - EasyMock.expect(httpFactory.getInstanceHttpClient()).andReturn( - new HttpClientInterceptor(new AssertCredentials(new UsernamePasswordCredentials("username", "password"))) - ).anyTimes(); - EasyMock.replay(httpFactory); - - BitbucketRepository repo = new BitbucketRepository("", builder); - repo.init(httpFactory); - - try { repo.postPullRequestApproval("prId"); } catch(Error e) { assertTrue(e instanceof AssertionError); } - } - - class MD5HasherFunction implements Function { - protected final MessageDigest MD5; - public MD5HasherFunction(MessageDigest md5) { this.MD5 = md5; } - public String apply(String f) { - try { return new String(Hex.encodeHex(MD5.digest(f.getBytes("UTF-8")))); } catch(UnsupportedEncodingException e) { } - return null; - } - } - - class SHA1HasherFunction implements Function { - protected final MessageDigest SHA1; - public SHA1HasherFunction(MessageDigest sha1) { this.SHA1 = sha1; } - public String apply(String f) { - try { return new String(Hex.encodeHex(SHA1.digest(f.getBytes("UTF-8")))); } catch(UnsupportedEncodingException e) { } - return null; - } - } - - @Test - public void repositoryProjectIdTest() throws ANTLRException, NoSuchAlgorithmException, UnsupportedEncodingException { - BitbucketBuildTrigger trigger = new BitbucketBuildTrigger( - "", "@hourly", - "JenkinsCID", - "foo", - "bar", - "", "", - "", true, - "jenkins", "Jenkins", "", - true, - true, - false, BitbucketRepository.DEFAULT_COMMENT_TRIGGER - ); - - BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); - EasyMock.expect(builder.getTrigger()).andReturn(trigger).anyTimes(); - - final MessageDigest MD5 = MessageDigest.getInstance("MD5"); - - String[] projectIds = new String[] { - "one", - "Second project", - "Project abstract 1.1", - "Good project, careated at " + (new java.util.Date()).toString(), - }; - - Collection hashedProjectIdsCollection = Collections2.transform(Arrays.asList(projectIds), new MD5HasherFunction(MD5)); - String[] hashedPojectIds = hashedProjectIdsCollection.toArray(new String[hashedProjectIdsCollection.size()]); - - for(String projectId : hashedPojectIds) { - EasyMock.expect(builder.getProjectId()).andReturn(projectId).times(1); - } - EasyMock.replay(builder); - - BitbucketRepository repo = new BitbucketRepository("", builder); - repo.init(); - - for(String projectId : projectIds) { - String hashMD5 = new String(Hex.encodeHex(MD5.digest(projectId.getBytes("UTF-8")))); - String buildStatusKey = repo.getClient().buildStatusKey(builder.getProjectId()); - - assertTrue(buildStatusKey.length() <= ApiClient.MAX_KEY_SIZE_BB_API); - assertEquals(buildStatusKey, "jenkins-" + hashMD5); - } - } - - @Test - public void triggerLongCIKeyTest() throws ANTLRException, NoSuchAlgorithmException { - BitbucketBuildTrigger trigger = new BitbucketBuildTrigger( - "", "@hourly", - "JenkinsCID", - "foo", - "bar", - "", "", - "", true, - "jenkins-too-long-ci-key", "Jenkins", "", - true, - true, - false, BitbucketRepository.DEFAULT_COMMENT_TRIGGER - ); - - final MessageDigest MD5 = MessageDigest.getInstance("MD5"); - final MessageDigest SHA1 = MessageDigest.getInstance("SHA1"); - - BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); - EasyMock.expect(builder.getTrigger()).andReturn(trigger).anyTimes(); - EasyMock.expect(builder.getProjectId()).andReturn((new MD5HasherFunction(MD5)).apply("projectId")).anyTimes(); - EasyMock.replay(builder); - - BitbucketRepository repo = new BitbucketRepository("", builder); - repo.init(); - - String buildStatusKey = repo.getClient().buildStatusKey(builder.getProjectId()); - assertTrue(buildStatusKey.length() <= ApiClient.MAX_KEY_SIZE_BB_API); - assertFalse(buildStatusKey.startsWith("jenkins-")); - assertEquals((new SHA1HasherFunction(SHA1)).apply("jenkins-too-long-ci-key" + "-" + builder.getProjectId()), buildStatusKey); - } - - @Test - public void getTargetPullRequestsWithNullDestinationCommit() throws Exception { - // arrange - - // setup mock BitbucketBuildTrigger - final BitbucketBuildTrigger trigger = EasyMock.createMock(BitbucketBuildTrigger.class); - EasyMock.expect(trigger.getCiSkipPhrases()).andReturn(""); - EasyMock.expect(trigger.getBranchesFilterBySCMIncludes()).andReturn(false); - EasyMock.expect(trigger.getBranchesFilter()).andReturn(""); - EasyMock.replay(trigger); - - // setup mock BitbucketPullRequestsBuilder - final BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); - EasyMock.expect(builder.getTrigger()).andReturn(trigger).anyTimes(); - EasyMock.expect(builder.getProjectId()).andReturn("").anyTimes(); - EasyMock.replay(builder); - - // setup PRs to return from mock ApiClient - final Pullrequest pullRequest = new Pullrequest(); - - final Pullrequest.Repository sourceRepo = new Pullrequest.Repository(); - sourceRepo.setFullName("Owner/Name"); - - final Pullrequest.Repository destRepo = new Pullrequest.Repository(); - destRepo.setFullName("Owner/Name"); - - final Pullrequest.Branch sourceBranch = new Pullrequest.Branch(); - sourceBranch.setName("Name"); - - final Pullrequest.Branch destBranch = new Pullrequest.Branch(); - destBranch.setName("Name"); - - final Pullrequest.Commit sourceCommit = new Pullrequest.Commit(); - sourceCommit.setHash("Hash"); - - final Pullrequest.Commit destCommit = null; // the crux of the test - - final Pullrequest.Revision sourceRevision = new Pullrequest.Revision(); - sourceRevision.setBranch(sourceBranch); - sourceRevision.setRepository(sourceRepo); - sourceRevision.setCommit(sourceCommit); - - final Pullrequest.Revision destRevision = new Pullrequest.Revision(); - destRevision.setBranch(destBranch); - destRevision.setRepository(destRepo); - destRevision.setCommit(destCommit); - - final Pullrequest.Author author = new Pullrequest.Author(); - author.setDisplayName("DisplayName"); - author.setUsername("Username"); - - pullRequest.setSource(sourceRevision); - pullRequest.setDestination(destRevision); - pullRequest.setId("Id"); - pullRequest.setTitle("Title"); - pullRequest.setState("OPEN"); - pullRequest.setAutohor(author); - - final List pullRequests = new ArrayList<>(Arrays.asList(pullRequest)); - - // setup mock ApiClient - final ApiClient client = EasyMock.createNiceMock(ApiClient.class); - EasyMock.expect(client.getPullRequests()).andReturn(pullRequests); - EasyMock.replay(client); - - // setup SUT - final BitbucketRepository repo = new BitbucketRepository("", builder); - - // act - repo.init(client); - - // assert - Collection targetPullRequests = repo.getTargetPullRequests(); - - assertEquals(pullRequests.size(), targetPullRequests.size()); - assertEquals(pullRequest, targetPullRequests.iterator().next()); - } -} -- cgit v1.2.3