├── .gitignore ├── .travis.yml ├── COPYING ├── Jenkinsfile ├── README.md ├── pom.xml └── src ├── main ├── java │ └── bitbucketpullrequestbuilder │ │ └── bitbucketpullrequestbuilder │ │ ├── .gitignore │ │ ├── BitbucketAdditionalParameterEnvironmentContributor.java │ │ ├── BitbucketBuildFilter.java │ │ ├── BitbucketBuildListener.java │ │ ├── BitbucketBuildTrigger.java │ │ ├── BitbucketBuilds.java │ │ ├── BitbucketCause.java │ │ ├── BitbucketPullRequestsBuilder.java │ │ ├── BitbucketRepository.java │ │ └── bitbucket │ │ ├── AbstractPullrequest.java │ │ ├── ApiClient.java │ │ ├── BuildState.java │ │ ├── cloud │ │ ├── CloudApiClient.java │ │ ├── CloudBitbucketCause.java │ │ └── CloudPullrequest.java │ │ └── server │ │ ├── ServerApiClient.java │ │ ├── ServerBitbucketCause.java │ │ └── ServerPullrequest.java └── resources │ ├── bitbucketpullrequestbuilder │ └── bitbucketpullrequestbuilder │ │ └── BitbucketBuildTrigger │ │ ├── config.jelly │ │ ├── help-bitbucketServer.html │ │ ├── help-branchesFilter.html │ │ ├── help-branchesFilterBySCMIncludes.html │ │ ├── help-buildChronologically.html │ │ ├── help-cancelOutdatedJobs.html │ │ ├── help-ciKey.html │ │ ├── help-ciName.html │ │ ├── help-ciSkipPhrases.html │ │ └── help.html │ └── index.jelly └── test └── java ├── BitbucketBuildFilterTest.java ├── BitbucketBuildRepositoryTest.java └── BitbucketCloudTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | bitbucket-pullrequest-builder.iml 3 | target/ 4 | work 5 | .settings 6 | .classpath 7 | .factorypath 8 | .project 9 | .vscode 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | dist: trusty 4 | 5 | jdk: 6 | - openjdk8 7 | - oraclejdk8 8 | 9 | script: 10 | mvn install -U 11 | 12 | after_failure: 13 | - cat target/surefire-reports/*.txt 14 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright 2014 Shinsuke Nishio. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY SHINSUKE NISHIO "AS IS" AND ANY EXPRESS OR IMPLIED 14 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 15 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 16 | EVENT SHALL SHINSUKE NISHIO OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 17 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 18 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 19 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 20 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 21 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 22 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | // For Jenkinsci build 2 | buildPlugin() 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Bitbucket Pull Request Builder Plugin 2 | ===================================== 3 | 4 | This Jenkins plugin builds pull requests from Bitbucket.org and will report the test results. 5 | 6 | ### Current Maintainer(s): 7 | ***This repo is looking for a new maintainer.*** 8 | 9 | - [David Frascone](https://github.com/CodeMonk) - No longer has access to a jenkins system, due to a job change, and can no longer test any changes. 10 | 11 | ### Build Status 12 | 13 | [![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) 14 | 15 | 16 | Prerequisites 17 | ------------- 18 | 19 | - Jenkins 1.625.3 or higher. 20 | - https://wiki.jenkins-ci.org/display/JENKINS/Git+Plugin 21 | 22 | 23 | Creating a Job 24 | ------------- 25 | 26 | - Create a new job 27 | - Select and configure Git SCM 28 | - Add Repository URL, `git@bitbucket.org:${repositoryOwner}/${repositoryName}.git` 29 | - In Branch Specifier, type `*/${sourceBranch}` 30 | - Under Build Triggers, check Bitbucket Pull Request Builder 31 | - In Cron, enter crontab for this job. 32 | - e.g. `* * * * *` will check for new pull requests every minute 33 | - In Bitbucket BasicAuth Username, write your bitbucket username, like `jenkins@densan-labs.net` 34 | - In Bitbucket BasicAuth Password, write your password 35 | - In CI Identifier, enter an unique identifier among your Jenkins jobs related to the repo 36 | - In CI Name, enter a human readable name for your Jenkins server 37 | - Write RepositoryOwner 38 | - Write RepositoryName 39 | - Save to preserve your changes 40 | 41 | 42 | Jenkins pipeline 43 | ------------- 44 | ``` 45 | pipeline { 46 | agent any 47 | triggers{ 48 | bitbucketpr(projectPath:'', 49 | bitbucketServer:'', 50 | cron: 'H/15 * * * *', 51 | credentialsId: '', 52 | username: '', 53 | password: '', 54 | repositoryOwner: '', 55 | repositoryName: '', 56 | branchesFilter: '', 57 | branchesFilterBySCMIncludes: false, 58 | ciKey: '', 59 | ciName: '', 60 | ciSkipPhrases: '', 61 | checkDestinationCommit: false, 62 | approveIfSuccess: false, 63 | cancelOutdatedJobs: true, 64 | buildChronologically: true, 65 | commentTrigger: '') 66 | } 67 | } 68 | ``` 69 | Note that the `projectPath` parameter does not need to be set if `bitbucketServer`, `repositoryOwner`, and 70 | `repositoryName` is set. 71 | 72 | You can use jenkins credentials by setting environment variables in the `environment` section 73 | and referring to them like for example `"${env.BITBUCKET_CREDENTIALS_USR}"`. 74 | 75 | After you set up your Jenkins pipeline, run the job for the first time manually (otherwise the trigger may not work!) 76 | 77 | 78 | Merge the Pull Request's Source Branch into the Target Branch Before Building 79 | ----------------------------------------------------------------------------- 80 | 81 | You may want Jenkins to attempt to merge your PR before building. 82 | This may help expose inconsistencies between the source branch and target branch. 83 | Note that if the merge cannot be completed, the build will fail immediately. 84 | 85 | - Follow the steps above in "Creating a Job" 86 | - In the "Source Code Management" > "Git" > "Additional Behaviors" section, click "Add" > "Merge Before Building" 87 | - 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"). 88 | - In "Branch to merge to" put "${targetBranch}" 89 | - Note that as long as you don't push these changes to your remote repository, the merge only happens in your local repository. 90 | 91 | 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. 92 | - There is a checkbox that says, "Rebuild if destination branch changes?" which enables this check. 93 | 94 | 95 | Rerun a Build 96 | ------------- 97 | 98 | If you want to rerun a pull request build, write a comment on your pull request reading “test this please”. 99 | 100 | 101 | Environment Variables Provided 102 | ------------------------------ 103 | 104 | - `sourceBranch` 105 | - `targetBranch` 106 | - `repositoryOwner` 107 | - `repositoryName` 108 | - `pullRequestId` 109 | - `destinationRepositoryOwner` 110 | - `destinationRepositoryName` 111 | - `pullRequestTitle` 112 | - `pullRequestAuthor` 113 | 114 | 115 | Contributing to Bitbucket Pull Request Builder Plugin 116 | ----------------------------------------------------- 117 | 118 | - Do not use Fork [jenkinsci/bitbucket-pullrequest-builder-plugin](https://github.com/jenkinsci/bitbucket-pullrequest-builder-plugin) for contribution 119 | 120 | - Use project [nishio-dens/bitbucket-pullrequest-builder-plugin](https://github.com/nishio-dens/bitbucket-pullrequest-builder-plugin) 121 | 122 | - Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet. 123 | 124 | - Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it. 125 | 126 | - Fork the project. 127 | 128 | - Start a feature/bugfix branch. 129 | 130 | - Commit and push until you are happy with your contribution. 131 | 132 | 133 | 134 | Donations 135 | ----------------------------------------------------- 136 | Do you like this plugin? feel free to donate! 137 | 138 | Paypal: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=LTXCF78GJ7224 139 | 140 | BTC: 1KgwyVzefeNzJhuzqLq36E3bZi2WFjibMr 141 | 142 | Thank you! 143 | 144 | Copyright 145 | --------- 146 | 147 | Copyright © 2022 S.nishio + Martin Damovsky + David Frascone 148 | 149 | 150 | License 151 | ------- 152 | 153 | - BSD License 154 | - See COPYING file 155 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | org.jenkins-ci.plugins 5 | plugin 6 | 2.11 7 | 8 | 9 | 10 | 3.0.4 11 | false 12 | 13 | 14 | bitbucket-pullrequest-builder 15 | Bitbucket Pullrequest Builder Plugin 16 | 1.5.1-SNAPSHOT 17 | This Jenkins plugin builds pull requests from Bitbucket.org and will report the test results. 18 | hpi 19 | https://wiki.jenkins-ci.org/display/JENKINS/Bitbucket+pullrequest+builder+plugin 20 | 21 | 22 | scm:git:ssh://git@github.com/jenkinsci/bitbucket-pullrequest-builder-plugin.git 23 | scm:git:ssh://git@github.com/jenkinsci/bitbucket-pullrequest-builder-plugin.git 24 | https://github.com/jenkinsci/bitbucket-pullrequest-builder-plugin.git 25 | bitbucket-pullrequest-builder-1.4.30 26 | 27 | 28 | 29 | nishio_dens 30 | nishio_dens 31 | nishio@densan-labs.net 32 | 33 | 34 | damovsky 35 | Martin Damovsky 36 | martin.damovsky@gmail.com 37 | 38 | 39 | 40 | 41 | 42 | 43 | repo.jenkins-ci.org 44 | https://repo.jenkins-ci.org/public/ 45 | 46 | 47 | 48 | 49 | 50 | com.google.code.findbugs 51 | annotations 52 | 3.0.1u2 53 | provided 54 | 55 | 56 | org.apache.maven.wagon 57 | wagon-http 58 | 3.5.2 59 | 60 | 61 | commons-httpclient 62 | commons-httpclient 63 | 20020423 64 | 65 | 66 | commons-codec 67 | commons-codec 68 | 20041127.091804 69 | 70 | 71 | org.codehaus.jackson 72 | jackson-jaxrs 73 | 1.9.13-atlassian-2 74 | 75 | 76 | org.jenkins-ci.plugins 77 | git 78 | 4.11.4 79 | 80 | 81 | org.jenkins-ci 82 | symbol-annotation 83 | 1.23 84 | 85 | 86 | com.google.guava 87 | guava 88 | 32.0.0-jre 89 | 90 | 91 | org.easymock 92 | easymock 93 | 4.3 94 | test 95 | 96 | 97 | 98 | 99 | 100 | 101 | org.codehaus.mojo 102 | findbugs-maven-plugin 103 | 3.0.4 104 | 105 | true 106 | ${findbugs.failOnError} 107 | 108 | 109 | 110 | run-findbugs 111 | verify 112 | 113 | check 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | repo.jenkins-ci.org 124 | https://repo.jenkins-ci.org/public/ 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | work/ 3 | 4 | # 5 | # Eclipse metadata. 6 | # 7 | .project 8 | .classpath 9 | .settings/ 10 | 11 | # 12 | # Eclipse and Maven build results 13 | # 14 | bin/ 15 | 16 | # IntelliJ metadata. 17 | *.iml 18 | *.ipr 19 | *.iws 20 | .idea/ 21 | -------------------------------------------------------------------------------- /src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketAdditionalParameterEnvironmentContributor.java: -------------------------------------------------------------------------------- 1 | package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; 2 | 3 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.cloud.CloudBitbucketCause; 4 | import hudson.EnvVars; 5 | import hudson.Extension; 6 | import hudson.model.*; 7 | 8 | import java.io.IOException; 9 | 10 | @Extension 11 | public class BitbucketAdditionalParameterEnvironmentContributor extends EnvironmentContributor { 12 | @Override 13 | public void buildEnvironmentFor(Run run, EnvVars envVars, TaskListener taskListener) 14 | throws IOException, InterruptedException { 15 | 16 | BitbucketCause cause = (BitbucketCause) run.getCause(BitbucketCause.class); 17 | if (cause == null) { 18 | return; 19 | } 20 | 21 | putEnvVar(envVars, "sourceBranch", cause.getSourceBranch()); 22 | putEnvVar(envVars, "targetBranch", cause.getTargetBranch()); 23 | putEnvVar(envVars, "repositoryOwner", cause.getRepositoryOwner()); 24 | putEnvVar(envVars, "repositoryName", cause.getRepositoryName()); 25 | putEnvVar(envVars, "pullRequestId", cause.getPullRequestId()); 26 | putEnvVar(envVars, "destinationRepositoryOwner", cause.getDestinationRepositoryOwner()); 27 | putEnvVar(envVars, "destinationRepositoryName", cause.getDestinationRepositoryName()); 28 | putEnvVar(envVars, "pullRequestTitle", cause.getPullRequestTitle()); 29 | putEnvVar(envVars, "pullRequestAuthor", cause.getPullRequestAuthor()); 30 | 31 | } 32 | 33 | private static void putEnvVar(EnvVars envs, String name, String value) { 34 | envs.put(name, getString(value, "")); 35 | } 36 | 37 | private static String getString(String actual, String d) { 38 | return actual == null ? d : actual; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildFilter.java: -------------------------------------------------------------------------------- 1 | package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; 2 | 3 | import java.util.logging.Logger; 4 | import java.util.regex.Pattern; 5 | import java.util.ArrayList; 6 | import java.util.Collection; 7 | import java.util.List; 8 | import java.util.logging.Level; 9 | import java.util.regex.Matcher; 10 | 11 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.cloud.CloudBitbucketCause; 12 | import jenkins.plugins.git.AbstractGitSCMSource; 13 | import jenkins.scm.api.SCMSource; 14 | 15 | /** 16 | * Mutable wrapper 17 | */ 18 | class Mutable { 19 | private T value; 20 | public Mutable() { this.value = null; } 21 | public Mutable(T value) { this.value = value; } 22 | T get() { return this.value; } 23 | void set(T value) { this.value = value; } 24 | } 25 | 26 | abstract class Filter { 27 | protected static final Logger logger = Logger.getLogger(BitbucketBuildTrigger.class.getName()); 28 | 29 | public static final String RX_FILTER_FLAG = "r"; 30 | public static final String RX_FILTER_FLAG_SINGLE = RX_FILTER_FLAG + ":"; 31 | 32 | public static final String SRC_RX = "s:(" + RX_FILTER_FLAG_SINGLE + ")?"; 33 | public static final String DST_RX = "d:(" + RX_FILTER_FLAG_SINGLE + ")?"; 34 | public static final String AUTHOR_RX = "a:(" + RX_FILTER_FLAG_SINGLE + ")?"; 35 | public static final String BRANCH_FILTER_RX_PART = "([^\\s$]*)"; 36 | 37 | abstract public boolean apply(String filter, BitbucketCause cause); 38 | abstract public boolean check(String filter); 39 | 40 | static final Pattern RX_SRC_DST_PARTS = Pattern.compile("(s:)|(d:)"); 41 | public static boolean HasSourceOrDestPartsPredicate(String filter) { return RX_SRC_DST_PARTS.matcher(filter).find(); } 42 | 43 | static final Pattern RX_AUTHOR_PARTS = Pattern.compile("(a:)"); 44 | public static boolean HasAuthorPartsPredicate(String filter) { return RX_AUTHOR_PARTS.matcher(filter).find(); } 45 | 46 | protected boolean applyByRx(Pattern rx, Filter usedFilter, String filter, BitbucketCause cause) { 47 | Matcher srcMatch = rx.matcher(filter); 48 | boolean apply = false; 49 | while (srcMatch.find()) { 50 | String computedFilter = ((srcMatch.group(1) == null ? "" : srcMatch.group(1)) + srcMatch.group(2)).trim(); 51 | logger.log(Level.FINE, "Apply computed filter: {0}", computedFilter); 52 | apply = apply || (computedFilter.isEmpty() ? true : usedFilter.apply(computedFilter, cause)); 53 | } 54 | return apply; 55 | } 56 | } 57 | 58 | class EmptyFilter extends Filter { 59 | @Override 60 | public boolean apply(String filter, BitbucketCause cause) { return true; } 61 | @Override 62 | public boolean check(String filter) { return true; } 63 | } 64 | 65 | class AnyFlag extends Filter { 66 | @Override 67 | public boolean apply(String filter, BitbucketCause cause) { return true; } 68 | @Override 69 | public boolean check(String filter) { return filter.isEmpty() || filter.contains("*") || filter.toLowerCase().contains("any"); } 70 | } 71 | 72 | class OnlySourceFlag extends Filter { 73 | @Override 74 | public boolean apply(String filter, BitbucketCause cause) { 75 | String selectedRx = filter.startsWith(RX_FILTER_FLAG_SINGLE) ? filter.substring(RX_FILTER_FLAG_SINGLE.length()) : Pattern.quote(filter); 76 | logger.log(Level.FINE, "OnlySourceFlag using filter: {0}", selectedRx); 77 | Matcher matcher = Pattern.compile(selectedRx, Pattern.CASE_INSENSITIVE).matcher(cause.getSourceBranch()); 78 | return filter.startsWith(RX_FILTER_FLAG_SINGLE) ? matcher.find() : matcher.matches(); 79 | } 80 | @Override 81 | public boolean check(String filter) { 82 | return false; 83 | } 84 | } 85 | 86 | class OnlyDestFlag extends Filter { 87 | @Override 88 | public boolean apply(String filter, BitbucketCause cause) { 89 | String selectedRx = filter.startsWith(RX_FILTER_FLAG_SINGLE) ? filter.substring(RX_FILTER_FLAG_SINGLE.length()) : Pattern.quote(filter); 90 | logger.log(Level.FINE, "OnlyDestFlag using filter: {0}", selectedRx); 91 | Matcher matcher = Pattern.compile(selectedRx, Pattern.CASE_INSENSITIVE).matcher(cause.getTargetBranch()); 92 | return filter.startsWith(RX_FILTER_FLAG_SINGLE) ? matcher.find() : matcher.matches(); 93 | } 94 | @Override 95 | public boolean check(String filter) { 96 | return !HasSourceOrDestPartsPredicate(filter); 97 | } 98 | } 99 | 100 | class SourceDestFlag extends Filter { 101 | static final Pattern SRC_MATCHER_RX = Pattern.compile(SRC_RX + BRANCH_FILTER_RX_PART, Pattern.CASE_INSENSITIVE | Pattern.CANON_EQ); 102 | static final Pattern DST_MATCHER_RX = Pattern.compile(DST_RX + BRANCH_FILTER_RX_PART, Pattern.CASE_INSENSITIVE | Pattern.CANON_EQ); 103 | 104 | @Override 105 | public boolean apply(String filter, BitbucketCause cause) { 106 | return this.applyByRx(SRC_MATCHER_RX, new OnlySourceFlag(), filter, cause) && 107 | this.applyByRx(DST_MATCHER_RX, new OnlyDestFlag(), filter, cause); 108 | } 109 | @Override 110 | public boolean check(String filter) { 111 | return HasSourceOrDestPartsPredicate(filter); 112 | } 113 | } 114 | 115 | class AuthorFlag extends Filter { 116 | static final Pattern AUTHOR_MATCHER_RX = Pattern.compile(AUTHOR_RX + BRANCH_FILTER_RX_PART, Pattern.CASE_INSENSITIVE | Pattern.CANON_EQ); 117 | 118 | static class AuthorFlagImpl extends Filter { 119 | @Override 120 | public boolean apply(String filter, BitbucketCause cause) { 121 | String selectedRx = filter.startsWith(RX_FILTER_FLAG_SINGLE) ? filter.substring(RX_FILTER_FLAG_SINGLE.length()) : Pattern.quote(filter); 122 | logger.log(Level.FINE, "AuthorFlagImpl using filter: {0}", selectedRx); 123 | Matcher matcher = Pattern.compile(selectedRx, Pattern.CASE_INSENSITIVE).matcher(cause.getPullRequestAuthor()); 124 | return filter.startsWith(RX_FILTER_FLAG_SINGLE) ? matcher.find() : matcher.matches(); 125 | } 126 | @Override 127 | public boolean check(String filter) { return false; } 128 | } 129 | 130 | @Override 131 | public boolean apply(String filter, BitbucketCause cause) { 132 | return this.applyByRx(AUTHOR_MATCHER_RX, new AuthorFlagImpl(), filter, cause); 133 | } 134 | @Override 135 | public boolean check(String filter) { 136 | return HasAuthorPartsPredicate(filter); 137 | } 138 | } 139 | 140 | class CombinedFlags extends Filter { 141 | private final Filter[] _filters; 142 | public CombinedFlags(Filter[] filters) { 143 | _filters = filters; 144 | } 145 | 146 | @Override 147 | public boolean apply(String filter, BitbucketCause cause) { 148 | boolean applied = true; 149 | for(Filter f: _filters) 150 | if (f.check(filter)) 151 | applied = applied && f.apply(filter, cause); 152 | return applied; 153 | } 154 | @Override 155 | public boolean check(String filter) { 156 | for(Filter f: _filters) 157 | if (f.check(filter)) 158 | return true; 159 | return false; 160 | } 161 | } 162 | 163 | /** 164 | * Created by maxvodo 165 | */ 166 | public class BitbucketBuildFilter { 167 | private static final Logger logger = Logger.getLogger(BitbucketBuildTrigger.class.getName()); 168 | 169 | private final String filter; 170 | private Filter currFilter = null; 171 | private static final List AvailableFilters; 172 | 173 | static { 174 | ArrayList filters = new ArrayList(); 175 | 176 | filters.add(new AnyFlag()); 177 | filters.add(new CombinedFlags(new Filter[] { 178 | new SourceDestFlag(), 179 | new AuthorFlag() 180 | })); 181 | filters.add(new OnlyDestFlag()); 182 | filters.add(new EmptyFilter()); 183 | 184 | AvailableFilters = filters; 185 | } 186 | 187 | public BitbucketBuildFilter(String f) { 188 | this.filter = (f != null ? f : "").trim(); 189 | this.buildFilter(this.filter); 190 | } 191 | 192 | private void buildFilter(String filter) { 193 | logger.log(Level.FINE, "Build filter by phrase: {0}", filter); 194 | for(Filter f : AvailableFilters) { 195 | if (f.check(filter)) { 196 | this.currFilter = f; 197 | logger.log(Level.FINE, "Using filter: {0}", f.getClass().getSimpleName()); 198 | break; 199 | } 200 | } 201 | } 202 | 203 | public boolean approved(BitbucketCause cause) { 204 | logger.log(Level.FINE, "Approve cause: {0}", cause.toString()); 205 | return this.currFilter.apply(this.filter, cause); 206 | } 207 | 208 | public static BitbucketBuildFilter instanceByString(String filter) { 209 | logger.log(Level.FINE, "Filter instance by filter string"); 210 | return new BitbucketBuildFilter(filter); 211 | } 212 | 213 | static public String filterFromGitSCMSource(AbstractGitSCMSource gitscm, String defaultFilter) { 214 | if (gitscm == null) { 215 | logger.log(Level.FINE, "Git SCMSource unavailable. Using default value: {0}", defaultFilter); 216 | return defaultFilter; 217 | } 218 | 219 | StringBuffer filter = new StringBuffer(defaultFilter); 220 | final String includes = gitscm.getIncludes(); 221 | if (includes != null && !includes.isEmpty()) { 222 | for(String part : includes.split("\\s+")) { 223 | filter.append(String.format("%s ", part.replaceAll("\\*\\/", "d:"))); 224 | } 225 | } 226 | 227 | logger.log(Level.FINE, "Git includes transformation to filter result: {1} -> {0}; default: {2}", new Object[]{ filter, includes, defaultFilter }); 228 | return filter.toString().trim(); 229 | } 230 | 231 | public static BitbucketBuildFilter instanceBySCM(Collection scmSources, String defaultFilter) { 232 | logger.log(Level.FINE, "Filter instance by using SCMSources list with {0} items", scmSources.size()); 233 | AbstractGitSCMSource gitscm = null; 234 | for(SCMSource scm : scmSources) { 235 | logger.log(Level.FINE, "Check {0} SCMSource ", scm.getClass()); 236 | if (scm instanceof AbstractGitSCMSource) { 237 | gitscm = (AbstractGitSCMSource)scm; 238 | break; 239 | } 240 | } 241 | return new BitbucketBuildFilter(filterFromGitSCMSource(gitscm, defaultFilter)); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildListener.java: -------------------------------------------------------------------------------- 1 | package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; 2 | 3 | import hudson.Extension; 4 | import hudson.model.Job; 5 | import hudson.model.Run; 6 | import hudson.model.TaskListener; 7 | import hudson.model.listeners.RunListener; 8 | import hudson.triggers.Trigger; 9 | import jenkins.model.ParameterizedJobMixIn; 10 | 11 | import javax.annotation.Nonnull; 12 | import java.util.logging.Logger; 13 | 14 | /** 15 | * Created by nishio 16 | */ 17 | @Extension 18 | public class BitbucketBuildListener extends RunListener> { 19 | private static final Logger logger = Logger.getLogger(BitbucketBuildListener.class.getName()); 20 | 21 | @Override 22 | public void onStarted(Run r, TaskListener listener) { 23 | logger.fine("BitbucketBuildListener onStarted called."); 24 | BitbucketBuilds builds = builds(r); 25 | if (builds != null) { 26 | builds.onStarted((BitbucketCause) r.getCause(BitbucketCause.class), r); 27 | } 28 | } 29 | 30 | @Override 31 | public void onCompleted(Run r, @Nonnull TaskListener listener) { 32 | logger.fine("BitbucketBuildListener onCompleted called."); 33 | BitbucketBuilds builds = builds(r); 34 | if (builds != null) { 35 | builds.onCompleted((BitbucketCause) r.getCause(BitbucketCause.class), r.getResult(), r.getUrl()); 36 | } 37 | } 38 | 39 | private BitbucketBuilds builds(Run r) { 40 | BitbucketBuildTrigger trigger = null; 41 | 42 | Job job = r.getParent(); 43 | if (job instanceof ParameterizedJobMixIn.ParameterizedJob) { 44 | for (Trigger t : ((ParameterizedJobMixIn.ParameterizedJob) job).getTriggers().values()) { 45 | if (t instanceof BitbucketBuildTrigger) { 46 | trigger = (BitbucketBuildTrigger) t; 47 | } 48 | } 49 | } 50 | 51 | return trigger == null ? null : trigger.getBuilder().getBuilds(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger.java: -------------------------------------------------------------------------------- 1 | package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; 2 | 3 | import static com.cloudbees.plugins.credentials.CredentialsMatchers.instanceOf; 4 | 5 | import java.io.IOException; 6 | import java.net.URISyntaxException; 7 | import java.util.ArrayList; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.concurrent.ExecutorService; 12 | import java.util.concurrent.Executors; 13 | import java.util.logging.Level; 14 | import java.util.logging.Logger; 15 | 16 | import javax.annotation.Nonnull; 17 | import javax.annotation.Nullable; 18 | 19 | import com.cloudbees.plugins.credentials.CredentialsProvider; 20 | import com.cloudbees.plugins.credentials.common.StandardListBoxModel; 21 | import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; 22 | import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials; 23 | import com.cloudbees.plugins.credentials.domains.DomainRequirement; 24 | 25 | import org.apache.commons.lang.StringUtils; 26 | import org.eclipse.jgit.transport.URIish; 27 | import org.jenkinsci.Symbol; 28 | import org.kohsuke.stapler.DataBoundConstructor; 29 | import org.kohsuke.stapler.StaplerRequest; 30 | 31 | import antlr.ANTLRException; 32 | import hudson.Extension; 33 | import hudson.model.Cause; 34 | import hudson.model.CauseAction; 35 | import hudson.model.Executor; 36 | import hudson.model.Item; 37 | import hudson.model.Job; 38 | import hudson.model.ParameterDefinition; 39 | import hudson.model.ParameterValue; 40 | import hudson.model.ParametersAction; 41 | import hudson.model.ParametersDefinitionProperty; 42 | import hudson.model.Queue; 43 | import hudson.model.Result; 44 | import hudson.model.Run; 45 | import hudson.model.queue.QueueTaskFuture; 46 | import hudson.plugins.git.RevisionParameterAction; 47 | import hudson.security.ACL; 48 | import hudson.triggers.Trigger; 49 | import hudson.triggers.TriggerDescriptor; 50 | import hudson.util.ListBoxModel; 51 | import jenkins.model.Jenkins; 52 | import jenkins.model.ParameterizedJobMixIn; 53 | import net.sf.json.JSONObject; 54 | 55 | /** 56 | * Created by nishio 57 | */ 58 | public class BitbucketBuildTrigger extends Trigger> { 59 | private static final Logger logger = Logger.getLogger(BitbucketBuildTrigger.class.getName()); 60 | private static final ExecutorService pool = Executors.newFixedThreadPool(5); 61 | 62 | private final String projectPath; 63 | private final String bitbucketServer; 64 | private final String cron; 65 | private final String credentialsId; 66 | private final String username; 67 | private final String password; 68 | private final String repositoryOwner; 69 | private final String repositoryName; 70 | private final String branchesFilter; 71 | private final boolean branchesFilterBySCMIncludes; 72 | private final String ciKey; 73 | private final String ciName; 74 | private final String ciSkipPhrases; 75 | private final boolean checkDestinationCommit; 76 | private final boolean approveIfSuccess; 77 | private final boolean cancelOutdatedJobs; 78 | private final boolean buildChronologically; 79 | private final String commentTrigger; 80 | 81 | transient private BitbucketPullRequestsBuilder bitbucketPullRequestsBuilder; 82 | 83 | public static final BitbucketBuildTriggerDescriptor descriptor = new BitbucketBuildTriggerDescriptor(); 84 | 85 | @DataBoundConstructor 86 | public BitbucketBuildTrigger( 87 | String projectPath, 88 | String bitbucketServer, 89 | String cron, 90 | String credentialsId, 91 | String username, 92 | String password, 93 | String repositoryOwner, 94 | String repositoryName, 95 | String branchesFilter, 96 | boolean branchesFilterBySCMIncludes, 97 | String ciKey, 98 | String ciName, 99 | String ciSkipPhrases, 100 | boolean checkDestinationCommit, 101 | boolean approveIfSuccess, 102 | boolean cancelOutdatedJobs, 103 | boolean buildChronologically, 104 | String commentTrigger 105 | ) throws ANTLRException { 106 | super(cron); 107 | this.projectPath = projectPath; 108 | this.bitbucketServer = bitbucketServer; 109 | this.cron = cron; 110 | this.credentialsId = credentialsId; 111 | this.username = username; 112 | this.password = password; 113 | this.repositoryOwner = repositoryOwner; 114 | this.repositoryName = repositoryName; 115 | this.branchesFilter = branchesFilter; 116 | this.branchesFilterBySCMIncludes = branchesFilterBySCMIncludes; 117 | this.ciKey = ciKey; 118 | this.ciName = ciName; 119 | this.ciSkipPhrases = ciSkipPhrases; 120 | this.checkDestinationCommit = checkDestinationCommit; 121 | this.approveIfSuccess = approveIfSuccess; 122 | this.cancelOutdatedJobs = cancelOutdatedJobs; 123 | this.buildChronologically = buildChronologically; 124 | this.commentTrigger = commentTrigger; 125 | } 126 | 127 | public String getProjectPath() { 128 | return this.projectPath; 129 | } 130 | 131 | public String getBitbucketServer() { 132 | return bitbucketServer; 133 | } 134 | 135 | public String getCron() { 136 | return this.cron; 137 | } 138 | 139 | public String getCredentialsId() { 140 | return credentialsId; 141 | } 142 | 143 | public String getUsername() { 144 | return username; 145 | } 146 | 147 | public String getPassword() { 148 | return password; 149 | } 150 | 151 | public String getRepositoryOwner() { 152 | return repositoryOwner; 153 | } 154 | 155 | public String getRepositoryName() { 156 | return repositoryName; 157 | } 158 | 159 | public String getBranchesFilter() { 160 | return branchesFilter; 161 | } 162 | 163 | public boolean getBranchesFilterBySCMIncludes() { 164 | return branchesFilterBySCMIncludes; 165 | } 166 | 167 | public String getCiKey() { 168 | return ciKey; 169 | } 170 | 171 | public String getCiName() { 172 | return ciName; 173 | } 174 | 175 | public String getCiSkipPhrases() { 176 | return ciSkipPhrases; 177 | } 178 | 179 | public boolean getCheckDestinationCommit() { 180 | return checkDestinationCommit; 181 | } 182 | 183 | public boolean getApproveIfSuccess() { 184 | return approveIfSuccess; 185 | } 186 | 187 | public boolean getCancelOutdatedJobs() { 188 | return cancelOutdatedJobs; 189 | } 190 | 191 | public boolean getBuildChronologically() { 192 | return buildChronologically; 193 | } 194 | 195 | /** 196 | * @return a phrase that when entered in a comment will trigger a new build 197 | */ 198 | public String getCommentTrigger() { 199 | return commentTrigger; 200 | } 201 | 202 | @Override 203 | public void start(Job job, boolean newInstance) { 204 | super.start(job, newInstance); 205 | 206 | try { 207 | this.bitbucketPullRequestsBuilder = BitbucketPullRequestsBuilder.getBuilder(); 208 | this.bitbucketPullRequestsBuilder.setJob(job); 209 | this.bitbucketPullRequestsBuilder.setTrigger(this); 210 | this.bitbucketPullRequestsBuilder.setupBuilder(); 211 | } catch(Exception e) { 212 | logger.log(Level.SEVERE, "Can't start trigger", e); 213 | return; 214 | } 215 | } 216 | 217 | public static BitbucketBuildTrigger getTrigger(Job job) { 218 | if (!(job instanceof ParameterizedJobMixIn.ParameterizedJob)) { 219 | return null; 220 | } 221 | 222 | ParameterizedJobMixIn.ParameterizedJob pjob = (ParameterizedJobMixIn.ParameterizedJob) job; 223 | 224 | Trigger trigger = pjob.getTriggers().get(descriptor); 225 | return (BitbucketBuildTrigger)trigger; 226 | } 227 | 228 | public BitbucketPullRequestsBuilder getBuilder() { 229 | return this.bitbucketPullRequestsBuilder; 230 | } 231 | 232 | public QueueTaskFuture startJob(BitbucketCause cause) { 233 | Map values = this.getDefaultParameters(); 234 | 235 | if (getCancelOutdatedJobs()) { 236 | cancelPreviousJobsInQueueThatMatch(cause); 237 | abortRunningJobsThatMatch(cause); 238 | } 239 | 240 | ParameterizedJobMixIn scheduledJob = new ParameterizedJobMixIn() { 241 | @Override 242 | protected Job asJob() { 243 | return job; 244 | } 245 | }; 246 | 247 | URIish repoUri = null; 248 | try { 249 | final String repositoryUri = cause.getRepositoryUri(); 250 | if (repositoryUri != null) { 251 | repoUri = new URIish(repositoryUri); 252 | } else { 253 | logger.log(Level.SEVERE, "Unable to create bitbucket url:{2}, checking out the pull request branch may fail. (n:{0} o:{1})", 254 | new Object[] { cause.getRepositoryName(), cause.getRepositoryOwner(), cause.getRepositoryUri() }); 255 | } 256 | } catch (URISyntaxException e) { 257 | logger.log(Level.SEVERE, "Unable to create URIish for bitbucket url:{2}, checking out the pull request branch may fail. (n:{0} o:{1}): {3}", 258 | new Object[] { cause.getRepositoryName(), cause.getRepositoryOwner(), cause.getRepositoryUri(), e.getMessage()}); 259 | } 260 | 261 | return scheduledJob.scheduleBuild2( 262 | this.getInstance().getQuietPeriod(), 263 | new CauseAction(cause), 264 | new ParametersAction(new ArrayList(values.values())), 265 | new RevisionParameterAction(cause.getSourceCommitHash(), repoUri) 266 | ); 267 | } 268 | 269 | private void cancelPreviousJobsInQueueThatMatch(@Nonnull BitbucketCause bitbucketCause) { 270 | logger.fine("Looking for queued jobs that match PR ID: " + bitbucketCause.getPullRequestId()); 271 | Queue queue = getInstance().getQueue(); 272 | 273 | for (Queue.Item item : queue.getItems()) { 274 | if (hasCauseFromTheSamePullRequest(item.getCauses(), bitbucketCause)) { 275 | logger.fine("Canceling item in queue: " + item); 276 | queue.cancel(item); 277 | } 278 | } 279 | } 280 | 281 | private Jenkins getInstance() { 282 | final Jenkins instance = Jenkins.getInstance(); 283 | if (instance == null){ 284 | throw new IllegalStateException("Jenkins instance is NULL!"); 285 | } 286 | return instance; 287 | } 288 | 289 | private void abortRunningJobsThatMatch(@Nonnull BitbucketCause bitbucketCause) { 290 | logger.fine("Looking for running jobs that match PR ID: " + bitbucketCause.getPullRequestId()); 291 | for (Object o : job.getBuilds()) { 292 | if (o instanceof Run) { 293 | Run build = (Run) o; 294 | if (build.isBuilding() && hasCauseFromTheSamePullRequest(build.getCauses(), bitbucketCause)) { 295 | logger.fine("Aborting build: " + build + " since PR is outdated"); 296 | setBuildDescription(build); 297 | final Executor executor = build.getExecutor(); 298 | if (executor == null){ 299 | throw new IllegalStateException("Executor can't be NULL"); 300 | } 301 | executor.interrupt(Result.ABORTED); 302 | } 303 | } 304 | } 305 | } 306 | 307 | private void setBuildDescription(final Run build) { 308 | try { 309 | build.setDescription("Aborting build by `Bitbucket Pullrequest Builder Plugin`: " + build + " since PR is outdated"); 310 | } catch (IOException e) { 311 | logger.warning("Can't set up build description due to an IOException: " + e.getMessage()); 312 | } 313 | } 314 | 315 | private boolean hasCauseFromTheSamePullRequest(@Nullable List causes, @Nullable BitbucketCause pullRequestCause) { 316 | if (causes != null && pullRequestCause != null) { 317 | for (Cause cause : causes) { 318 | if (cause instanceof BitbucketCause) { 319 | BitbucketCause sc = (BitbucketCause) cause; 320 | if (StringUtils.equals(sc.getPullRequestId(), pullRequestCause.getPullRequestId()) && 321 | StringUtils.equals(sc.getRepositoryName(), pullRequestCause.getRepositoryName())) { 322 | return true; 323 | } 324 | } 325 | } 326 | } 327 | return false; 328 | } 329 | 330 | private Map getDefaultParameters() { 331 | Map values = new HashMap(); 332 | ParametersDefinitionProperty definitionProperty = this.job.getProperty(ParametersDefinitionProperty.class); 333 | 334 | if (definitionProperty != null) { 335 | for (ParameterDefinition definition : definitionProperty.getParameterDefinitions()) { 336 | values.put(definition.getName(), definition.getDefaultParameterValue()); 337 | } 338 | } 339 | return values; 340 | } 341 | 342 | @Override 343 | public void run() { 344 | Job job = this.getBuilder().getJob(); 345 | String name = job.getFullName(); 346 | 347 | if (!job.isBuildable()) { 348 | logger.log(Level.FINE, "Build Skip for job - {0}.", name); 349 | } else { 350 | logger.log(Level.FINE, "running trigger for the job - {0}", name); 351 | 352 | pool.submit(new TriggerRunnable(this.getBuilder())); 353 | this.getDescriptor().save(); 354 | } 355 | } 356 | 357 | @Override 358 | public void stop() { 359 | super.stop(); 360 | } 361 | 362 | public boolean isCloud() { 363 | return StringUtils.isEmpty(bitbucketServer); 364 | } 365 | 366 | @Extension 367 | @Symbol("bitbucketpr") 368 | public static final class BitbucketBuildTriggerDescriptor extends TriggerDescriptor { 369 | public BitbucketBuildTriggerDescriptor() { 370 | load(); 371 | } 372 | 373 | @Override 374 | public boolean isApplicable(Item item) { 375 | return item instanceof Job && item instanceof ParameterizedJobMixIn.ParameterizedJob; 376 | } 377 | 378 | @Override 379 | public String getDisplayName() { 380 | return "Bitbucket Pull Requests Builder"; 381 | } 382 | 383 | @Override 384 | public boolean configure(StaplerRequest req, JSONObject json) throws FormException { 385 | save(); 386 | return super.configure(req, json); 387 | } 388 | 389 | public ListBoxModel doFillCredentialsIdItems() { 390 | return new StandardListBoxModel() 391 | .withEmptySelection() 392 | .withMatching( 393 | instanceOf(UsernamePasswordCredentials.class), 394 | CredentialsProvider.lookupCredentials( 395 | StandardUsernamePasswordCredentials.class, 396 | (Item) null, 397 | ACL.SYSTEM, 398 | (DomainRequirement) null 399 | ) 400 | ); 401 | } 402 | } 403 | 404 | private static final class TriggerRunnable implements Runnable { 405 | private final BitbucketPullRequestsBuilder builder; 406 | 407 | TriggerRunnable(BitbucketPullRequestsBuilder builder) { 408 | this.builder = builder; 409 | } 410 | 411 | @Override 412 | public void run() { 413 | synchronized (this) { 414 | this.builder.run(); 415 | } 416 | } 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuilds.java: -------------------------------------------------------------------------------- 1 | package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; 2 | 3 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.BuildState; 4 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.cloud.CloudBitbucketCause; 5 | import hudson.model.*; 6 | import jenkins.model.Jenkins; 7 | import jenkins.model.JenkinsLocationConfiguration; 8 | 9 | import java.io.IOException; 10 | import java.util.logging.Level; 11 | import java.util.logging.Logger; 12 | 13 | /** 14 | * Created by nishio 15 | */ 16 | public class BitbucketBuilds { 17 | private static final Logger logger = Logger.getLogger(BitbucketBuilds.class.getName()); 18 | private BitbucketBuildTrigger trigger; 19 | private BitbucketRepository repository; 20 | 21 | public BitbucketBuilds(BitbucketBuildTrigger trigger, BitbucketRepository repository) { 22 | this.trigger = trigger; 23 | this.repository = repository; 24 | this.repository.init(); 25 | } 26 | 27 | void onStarted(BitbucketCause cause, Run build) { 28 | if (cause == null) { 29 | return; 30 | } 31 | try { 32 | build.setDescription(cause.getShortDescription()); 33 | String buildUrl = getBuildUrl(build.getUrl()); 34 | repository.setBuildStatus(cause, BuildState.INPROGRESS, buildUrl); 35 | } catch (IOException e) { 36 | logger.log(Level.SEVERE, "Can't update build description", e); 37 | } 38 | } 39 | 40 | void onCompleted(BitbucketCause cause, Result result, String buildUrl) { 41 | if (cause == null) { 42 | return; 43 | } 44 | 45 | String fullBuildUrl = getBuildUrl(buildUrl); 46 | BuildState state = result == Result.SUCCESS ? BuildState.SUCCESSFUL : BuildState.FAILED; 47 | repository.setBuildStatus(cause, state, fullBuildUrl); 48 | 49 | if (this.trigger.getApproveIfSuccess() && result == Result.SUCCESS) { 50 | this.repository.postPullRequestApproval(cause.getPullRequestId()); 51 | } 52 | } 53 | 54 | private Jenkins getInstance() { 55 | final Jenkins instance = Jenkins.getInstance(); 56 | if (instance == null) { 57 | throw new IllegalStateException("Jenkins instance is NULL!"); 58 | } 59 | return instance; 60 | } 61 | 62 | private String getBuildUrl(String buildUrl) { 63 | String rootUrl = getInstance().getRootUrl(); 64 | if (rootUrl == null || "".equals(rootUrl)) { 65 | logger.log(Level.WARNING, "Jenkins Root URL is empty, please set it on Global Configuration"); 66 | return ""; 67 | } 68 | return rootUrl + buildUrl; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketCause.java: -------------------------------------------------------------------------------- 1 | package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; 2 | 3 | import hudson.model.Cause; 4 | 5 | public abstract class BitbucketCause extends Cause { 6 | private final String sourceBranch; 7 | private final String targetBranch; 8 | private final String repositoryOwner; 9 | private final String repositoryName; 10 | private final String repositoryUri; 11 | private final String pullRequestId; 12 | private final String destinationRepositoryOwner; 13 | private final String destinationRepositoryName; 14 | private final String pullRequestTitle; 15 | private final String sourceCommitHash; 16 | private final String destinationCommitHash; 17 | private final String pullRequestAuthor; 18 | 19 | protected BitbucketCause(String sourceBranch, String targetBranch, String repositoryOwner, String repositoryName, 20 | String repositoryUri, String pullRequestId, String destinationRepositoryOwner, 21 | String destinationRepositoryName, String pullRequestTitle, String sourceCommitHash, 22 | String destinationCommitHash, String pullRequestAuthor) { 23 | this.sourceBranch = sourceBranch; 24 | this.targetBranch = targetBranch; 25 | this.repositoryOwner = repositoryOwner; 26 | this.repositoryName = repositoryName; 27 | this.repositoryUri = repositoryUri; 28 | this.pullRequestId = pullRequestId; 29 | this.destinationRepositoryOwner = destinationRepositoryOwner; 30 | this.destinationRepositoryName = destinationRepositoryName; 31 | this.pullRequestTitle = pullRequestTitle; 32 | this.sourceCommitHash = sourceCommitHash; 33 | this.destinationCommitHash = destinationCommitHash; 34 | this.pullRequestAuthor = pullRequestAuthor; 35 | } 36 | 37 | public String getSourceBranch() { 38 | return sourceBranch; 39 | } 40 | 41 | public String getTargetBranch() { 42 | return targetBranch; 43 | } 44 | 45 | public String getRepositoryOwner() { 46 | return repositoryOwner; 47 | } 48 | 49 | public String getRepositoryName() { 50 | return repositoryName; 51 | } 52 | 53 | public String getRepositoryUri() { 54 | if (repositoryUri == null) { 55 | // If nil, generate it from our repo name and owner (assume bitbucket cloud) 56 | // Would be nice to go ahead and set repositoryUri too, for efficiency, but NOOOOOOO, it's gotta be final 57 | 58 | // repositoryUri = String.format("git@bitbucket.org:%s/%s.git", repositoryOwner, repositoryName); 59 | return String.format("git@bitbucket.org:%s/%s.git", repositoryOwner, repositoryName); 60 | } 61 | 62 | return repositoryUri; 63 | } 64 | 65 | public String getPullRequestId() { 66 | return pullRequestId; 67 | } 68 | 69 | public String getDestinationRepositoryOwner() { 70 | return destinationRepositoryOwner; 71 | } 72 | 73 | public String getDestinationRepositoryName() { 74 | return destinationRepositoryName; 75 | } 76 | 77 | public String getPullRequestTitle() { 78 | return pullRequestTitle; 79 | } 80 | 81 | public String getSourceCommitHash() { return sourceCommitHash; } 82 | 83 | public String getDestinationCommitHash() { return destinationCommitHash; } 84 | 85 | public String getPullRequestAuthor() { 86 | return this.pullRequestAuthor; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketPullRequestsBuilder.java: -------------------------------------------------------------------------------- 1 | package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; 2 | 3 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.AbstractPullrequest; 4 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.cloud.CloudPullrequest; 5 | 6 | import java.io.UnsupportedEncodingException; 7 | import java.security.MessageDigest; 8 | import java.security.NoSuchAlgorithmException; 9 | 10 | import java.util.Collection; 11 | import java.util.logging.Level; 12 | import java.util.logging.Logger; 13 | 14 | import hudson.model.Job; 15 | import org.apache.commons.codec.binary.Hex; 16 | 17 | /** 18 | * Created by nishio 19 | */ 20 | public class BitbucketPullRequestsBuilder { 21 | private static final Logger logger = Logger.getLogger(BitbucketBuildTrigger.class.getName()); 22 | private Job job; 23 | private BitbucketBuildTrigger trigger; 24 | private BitbucketRepository repository; 25 | private BitbucketBuilds builds; 26 | 27 | public static BitbucketPullRequestsBuilder getBuilder() { 28 | return new BitbucketPullRequestsBuilder(); 29 | } 30 | 31 | public void stop() { 32 | // TODO? 33 | } 34 | 35 | public void run() { 36 | this.repository.init(); 37 | Collection targetPullRequests = this.repository.getTargetPullRequests(); 38 | this.repository.addFutureBuildTasks(targetPullRequests); 39 | } 40 | 41 | public BitbucketPullRequestsBuilder setupBuilder() { 42 | if (this.job == null || this.trigger == null) { 43 | throw new IllegalStateException(); 44 | } 45 | this.repository = new BitbucketRepository(this.trigger.getProjectPath(), this); 46 | this.repository.init(); 47 | this.builds = new BitbucketBuilds(this.trigger, this.repository); 48 | return this; 49 | } 50 | 51 | public void setJob(Job job) { 52 | this.job = job; 53 | } 54 | 55 | public void setTrigger(BitbucketBuildTrigger trigger) { 56 | this.trigger = trigger; 57 | } 58 | 59 | public Job getJob() { 60 | return this.job; 61 | } 62 | 63 | /** 64 | * Return MD5 hashed full project name or full project name, if MD5 hash provider inaccessible 65 | * @return unique project id 66 | */ 67 | public String getProjectId() { 68 | try { 69 | final MessageDigest MD5 = MessageDigest.getInstance("MD5"); 70 | return new String(Hex.encodeHex(MD5.digest(this.job.getFullName().getBytes("UTF-8")))); 71 | } catch (NoSuchAlgorithmException exc) { 72 | logger.log(Level.WARNING, "Failed to produce hash", exc); 73 | } catch (UnsupportedEncodingException exc) { 74 | logger.log(Level.WARNING, "Failed to produce hash", exc); 75 | } 76 | return this.job.getFullName(); 77 | 78 | } 79 | 80 | public BitbucketBuildTrigger getTrigger() { 81 | return this.trigger; 82 | } 83 | 84 | public BitbucketBuilds getBuilds() { 85 | return this.builds; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketRepository.java: -------------------------------------------------------------------------------- 1 | package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; 2 | 3 | import static com.cloudbees.plugins.credentials.CredentialsMatchers.instanceOf; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collection; 7 | import java.util.Collections; 8 | import java.util.LinkedList; 9 | import java.util.List; 10 | import java.util.logging.Level; 11 | import java.util.logging.Logger; 12 | import java.util.regex.Matcher; 13 | import java.util.regex.Pattern; 14 | 15 | import com.cloudbees.plugins.credentials.CredentialsMatchers; 16 | import com.cloudbees.plugins.credentials.CredentialsProvider; 17 | import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; 18 | import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials; 19 | import com.cloudbees.plugins.credentials.domains.DomainRequirement; 20 | 21 | import org.apache.commons.lang.StringUtils; 22 | 23 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.AbstractPullrequest; 24 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.ApiClient; 25 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.BuildState; 26 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.cloud.CloudApiClient; 27 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.cloud.CloudBitbucketCause; 28 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.server.ServerApiClient; 29 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.server.ServerBitbucketCause; 30 | import hudson.model.Item; 31 | import hudson.security.ACL; 32 | import jenkins.model.Jenkins; 33 | import jenkins.scm.api.SCMSource; 34 | import jenkins.scm.api.SCMSourceOwner; 35 | import jenkins.scm.api.SCMSourceOwners; 36 | 37 | /** 38 | * Created by nishio 39 | */ 40 | public class BitbucketRepository { 41 | private static final Logger logger = Logger.getLogger(BitbucketRepository.class.getName()); 42 | private static final String BUILD_DESCRIPTION = "%s: %s into %s"; 43 | private static final String BUILD_REQUEST_DONE_MARKER = "ttp build flag"; 44 | private static final String BUILD_REQUEST_MARKER_TAG_SINGLE_RX = "\\#[\\w\\-\\d]+"; 45 | private static final String BUILD_REQUEST_MARKER_TAGS_RX = "\\[bid\\:\\s?(.*)\\]"; 46 | /** 47 | * Default value for comment trigger. 48 | */ 49 | public static final String DEFAULT_COMMENT_TRIGGER = "test this please"; 50 | 51 | private BitbucketPullRequestsBuilder builder; 52 | private BitbucketBuildTrigger trigger; 53 | private ApiClient client; 54 | 55 | public BitbucketRepository(String projectPath, BitbucketPullRequestsBuilder builder) { 56 | this.builder = builder; 57 | this.trigger = this.builder.getTrigger(); 58 | } 59 | 60 | public void init() { 61 | this.init(null, null); 62 | } 63 | 64 | public void init(T httpFactory) { 65 | this.init(null, httpFactory); 66 | } 67 | 68 | public void init(ApiClient client) { 69 | this.init(client, null); 70 | } 71 | 72 | public void init(ApiClient client, T httpFactory) { 73 | if (client == null) { 74 | String username = trigger.getUsername(); 75 | String password = trigger.getPassword(); 76 | StandardUsernamePasswordCredentials credentials = getCredentials(trigger.getCredentialsId()); 77 | if (credentials != null) { 78 | username = credentials.getUsername(); 79 | password = credentials.getPassword().getPlainText(); 80 | } 81 | 82 | if (this.trigger.isCloud()) { 83 | this.client = createCloudClient(httpFactory, username, password); 84 | } else { 85 | this.client = createServerClient(httpFactory, username, password); 86 | } 87 | } else this.client = client; 88 | } 89 | 90 | private CloudApiClient createCloudClient(T httpFactory, String username, String password) { 91 | return new CloudApiClient( 92 | username, 93 | password, 94 | trigger.getRepositoryOwner(), 95 | trigger.getRepositoryName(), 96 | trigger.getCiKey(), 97 | trigger.getCiName(), 98 | httpFactory 99 | ); 100 | } 101 | 102 | private ServerApiClient createServerClient(T httpFactory, String username, String password) { 103 | return new ServerApiClient( 104 | trigger.getBitbucketServer(), 105 | username, 106 | password, 107 | trigger.getRepositoryOwner(), 108 | trigger.getRepositoryName(), 109 | trigger.getCiKey(), 110 | trigger.getCiName(), 111 | httpFactory); 112 | } 113 | 114 | public List getTargetPullRequests() { 115 | logger.fine("Fetch PullRequests."); 116 | List pullRequests = client.getPullRequests(); 117 | List targetPullRequests = new ArrayList<>(); 118 | for(T pullRequest : pullRequests) { 119 | if (isBuildTarget(pullRequest)) { 120 | targetPullRequests.add(pullRequest); 121 | } 122 | } 123 | 124 | if (trigger.getBuildChronologically()){ 125 | Collections.reverse(targetPullRequests); 126 | } 127 | return targetPullRequests; 128 | } 129 | 130 | public ApiClient getClient() { 131 | return this.client; 132 | } 133 | 134 | public void addFutureBuildTasks(Collection pullRequests) { 135 | for(AbstractPullrequest pullRequest : pullRequests) { 136 | if ( this.trigger.getApproveIfSuccess() ) { 137 | deletePullRequestApproval(pullRequest.getId()); 138 | } 139 | 140 | final BitbucketCause cause = createCause(pullRequest); 141 | 142 | setBuildStatus(cause, BuildState.INPROGRESS, getInstance().getRootUrl()); 143 | this.builder.getTrigger().startJob(cause); 144 | } 145 | } 146 | 147 | private BitbucketCause createCause(AbstractPullrequest pullRequest) { 148 | // pullRequest.getDestination().getCommit() may return null for pull requests with merge conflicts 149 | // * see: https://github.com/nishio-dens/bitbucket-pullrequest-builder-plugin/issues/119 150 | // * see: https://github.com/nishio-dens/bitbucket-pullrequest-builder-plugin/issues/98 151 | final String destinationCommitHash; 152 | if (pullRequest.getDestination().getCommit() == null) { 153 | logger.log(Level.INFO, "Pull request #{0} ''{1}'' in repo ''{2}'' has a null value for destination commit.", 154 | new Object[]{pullRequest.getId(), pullRequest.getTitle(), pullRequest.getDestination().getRepository().getRepositoryName()}); 155 | destinationCommitHash = null; 156 | } else { 157 | destinationCommitHash = pullRequest.getDestination().getCommit().getHash(); 158 | } 159 | 160 | final AbstractPullrequest.RepositoryLinks links = pullRequest.getSource().getRepository().getLinks(); 161 | String repoUri = null; 162 | 163 | if(links != null) { 164 | final List cloneLinks = links.getCloneLinks(); 165 | if (cloneLinks != null) { 166 | for (AbstractPullrequest.RepositoryLink cloneLink : cloneLinks) { 167 | logger.log(Level.FINE, "Processing repository link: name=''{0}'', href=''{1}''.", 168 | new Object[] {cloneLink.getName(), cloneLink.getHref()}); 169 | if ( repoUri == null || "ssh".equals(cloneLink.getName()) ) { // prefer ssh URIs 170 | repoUri = cloneLink.getHref(); 171 | logger.log( 172 | Level.FINE, 173 | "Selected a new link value for the repository URI: name=''{0}'', href=''{1}''.", 174 | new Object[] {cloneLink.getName(), cloneLink.getHref()} 175 | ); 176 | } 177 | } 178 | } 179 | } 180 | 181 | logger.log(Level.INFO, "Using repository URI: ''{0}''.", new Object[] {repoUri}); 182 | 183 | final BitbucketCause cause; 184 | if (this.trigger.isCloud()) { 185 | cause = new CloudBitbucketCause( 186 | pullRequest.getSource().getBranch().getName(), 187 | pullRequest.getDestination().getBranch().getName(), 188 | pullRequest.getSource().getRepository().getOwnerName(), 189 | pullRequest.getSource().getRepository().getRepositoryName(), 190 | repoUri, 191 | pullRequest.getId(), 192 | pullRequest.getDestination().getRepository().getOwnerName(), 193 | pullRequest.getDestination().getRepository().getRepositoryName(), 194 | pullRequest.getTitle(), 195 | pullRequest.getSource().getCommit().getHash(), 196 | destinationCommitHash, 197 | pullRequest.getAuthor().getCombinedUsername() 198 | ); 199 | } else { 200 | cause = new ServerBitbucketCause( 201 | trigger.getBitbucketServer(), 202 | pullRequest.getSource().getBranch().getName(), 203 | pullRequest.getDestination().getBranch().getName(), 204 | pullRequest.getSource().getRepository().getOwnerName(), 205 | pullRequest.getSource().getRepository().getRepositoryName(), 206 | repoUri, 207 | pullRequest.getId(), 208 | pullRequest.getDestination().getRepository().getOwnerName(), 209 | pullRequest.getDestination().getRepository().getRepositoryName(), 210 | pullRequest.getTitle(), 211 | pullRequest.getSource().getCommit().getHash(), 212 | pullRequest.getDestination().getCommit().getHash(), 213 | pullRequest.getAuthor().getCombinedUsername() 214 | ); 215 | } 216 | return cause; 217 | } 218 | 219 | private Jenkins getInstance() { 220 | final Jenkins instance = Jenkins.getInstance(); 221 | if (instance == null){ 222 | throw new IllegalStateException("Jenkins instance is NULL!"); 223 | } 224 | return instance; 225 | } 226 | 227 | 228 | public void setBuildStatus(BitbucketCause cause, BuildState state, String buildUrl) { 229 | String comment = null; 230 | String sourceCommit = cause.getSourceCommitHash(); 231 | String owner = cause.getRepositoryOwner(); 232 | String repository = cause.getRepositoryName(); 233 | String destinationBranch = cause.getTargetBranch(); 234 | 235 | logger.fine("setBuildStatus " + state + " for commit: " + sourceCommit + " with url " + buildUrl); 236 | 237 | if (state == BuildState.FAILED || state == BuildState.SUCCESSFUL) { 238 | comment = String.format(BUILD_DESCRIPTION, builder.getJob().getDisplayName(), sourceCommit, destinationBranch); 239 | } 240 | 241 | this.client.setBuildStatus(owner, repository, sourceCommit, state, buildUrl, comment, this.builder.getProjectId()); 242 | } 243 | 244 | public void deletePullRequestApproval(String pullRequestId) { 245 | this.client.deletePullRequestApproval(pullRequestId); 246 | } 247 | 248 | public void postPullRequestApproval(String pullRequestId) { 249 | this.client.postPullRequestApproval(pullRequestId); 250 | } 251 | 252 | public String getMyBuildTag(String buildKey) { 253 | return "#" + this.client.buildStatusKey(buildKey); 254 | } 255 | 256 | final static Pattern BUILD_TAGS_RX = Pattern.compile(BUILD_REQUEST_MARKER_TAGS_RX, Pattern.CASE_INSENSITIVE | Pattern.CANON_EQ); 257 | final static Pattern SINGLE_BUILD_TAG_RX = Pattern.compile(BUILD_REQUEST_MARKER_TAG_SINGLE_RX, Pattern.CASE_INSENSITIVE | Pattern.CANON_EQ); 258 | final static String CONTENT_PART_TEMPLATE = "```[bid: %s]```"; 259 | 260 | private List getAvailableBuildTagsFromTTPComment(String buildTags) { 261 | logger.log(Level.FINE, "Parse {0}", new Object[]{ buildTags }); 262 | List availableBuildTags = new LinkedList(); 263 | Matcher subBuildTagMatcher = SINGLE_BUILD_TAG_RX.matcher(buildTags); 264 | while(subBuildTagMatcher.find()) availableBuildTags.add(subBuildTagMatcher.group(0).trim()); 265 | return availableBuildTags; 266 | } 267 | 268 | public boolean hasMyBuildTagInTTPComment(String content, String buildKey) { 269 | Matcher tagsMatcher = BUILD_TAGS_RX.matcher(content); 270 | if (tagsMatcher.find()) { 271 | logger.log(Level.FINE, "Content {0} g[1]:{1} mykey:{2}", new Object[] { content, tagsMatcher.group(1).trim(), this.getMyBuildTag(buildKey) }); 272 | return this.getAvailableBuildTagsFromTTPComment(tagsMatcher.group(1).trim()).contains(this.getMyBuildTag(buildKey)); 273 | } 274 | else return false; 275 | } 276 | 277 | private void postBuildTagInTTPComment(String pullRequestId, String content, String buildKey) { 278 | logger.log(Level.FINE, "Update build tag for {0} build key", buildKey); 279 | List builds = this.getAvailableBuildTagsFromTTPComment(content); 280 | builds.add(this.getMyBuildTag(buildKey)); 281 | content += " " + String.format(CONTENT_PART_TEMPLATE, StringUtils.join(builds, " ")); 282 | logger.log(Level.FINE, "Post comment: {0} with original content {1}", new Object[]{ content, this.client.postPullRequestComment(pullRequestId, content).getId() }); 283 | } 284 | 285 | private boolean isTTPComment(String content) { 286 | // special case: in unit tests, trigger is null and can't be mocked 287 | String commentTrigger = DEFAULT_COMMENT_TRIGGER; 288 | if(trigger != null && StringUtils.isNotBlank(trigger.getCommentTrigger())) { 289 | commentTrigger = trigger.getCommentTrigger(); 290 | } 291 | return content.contains(commentTrigger); 292 | } 293 | 294 | private boolean isTTPCommentBuildTags(String content) { 295 | return content.toLowerCase().contains(BUILD_REQUEST_DONE_MARKER.toLowerCase()); 296 | } 297 | 298 | public List filterPullRequestComments(List comments) { 299 | logger.fine("Filter PullRequest Comments."); 300 | Collections.sort(comments); 301 | Collections.reverse(comments); 302 | List filteredComments = new LinkedList(); 303 | for(AbstractPullrequest.Comment comment : comments) { 304 | String content = comment.getContent(); 305 | logger.log(Level.FINE, "Found comment: id:" + comment.getId() +" <" + comment.getContent() + ">"); 306 | if (content == null || content.isEmpty()) continue; 307 | boolean isTTP = this.isTTPComment(content); 308 | boolean isTTPBuild = this.isTTPCommentBuildTags(content); 309 | logger.log(Level.FINE, "isTTP: " + isTTP + " isTTPBuild: " + isTTPBuild); 310 | if (isTTP || isTTPBuild) filteredComments.add(comment); 311 | if (isTTP) break; 312 | } 313 | return filteredComments; 314 | } 315 | 316 | private boolean isBuildTarget(AbstractPullrequest pullRequest) { 317 | if (pullRequest.getState() != null && pullRequest.getState().equals("OPEN")) { 318 | if (isSkipBuild(pullRequest.getTitle()) || !isFilteredBuild(pullRequest)) { 319 | logger.log(Level.FINE, "Skipping build for " + pullRequest.getTitle() + 320 | ": skip:" + isSkipBuild(pullRequest.getTitle()) + " : isFilteredBuild: " + 321 | isFilteredBuild(pullRequest)); 322 | return false; 323 | } 324 | 325 | AbstractPullrequest.Revision source = pullRequest.getSource(); 326 | String sourceCommit = source.getCommit().getHash(); 327 | AbstractPullrequest.Revision destination = pullRequest.getDestination(); 328 | String owner = destination.getRepository().getOwnerName(); 329 | String repositoryName = destination.getRepository().getRepositoryName(); 330 | 331 | AbstractPullrequest.Repository sourceRepository = source.getRepository(); 332 | String buildKeyPart = this.builder.getProjectId(); 333 | 334 | final boolean commitAlreadyBeenProcessed = this.client.hasBuildStatus( 335 | sourceRepository.getOwnerName(), sourceRepository.getRepositoryName(), sourceCommit, buildKeyPart 336 | ); 337 | if (commitAlreadyBeenProcessed) logger.log(Level.FINE, 338 | "Commit {0}#{1} has already been processed", 339 | new Object[]{ sourceCommit, buildKeyPart } 340 | ); 341 | 342 | final String id = pullRequest.getId(); 343 | List comments = client.getPullRequestComments(owner, repositoryName, id); 344 | 345 | boolean rebuildCommentAvailable = false; 346 | if (comments != null) { 347 | Collection filteredComments = this.filterPullRequestComments(comments); 348 | boolean hasMyBuildTag = false; 349 | for (AbstractPullrequest.Comment comment : filteredComments) { 350 | String content = comment.getContent(); 351 | if (this.isTTPComment(content)) { 352 | rebuildCommentAvailable = true; 353 | logger.log(Level.FINE, 354 | "Rebuild comment available for commit {0} and comment #{1}", 355 | new Object[]{ sourceCommit, comment.getId() } 356 | ); 357 | } 358 | if (isTTPCommentBuildTags(content)) 359 | hasMyBuildTag |= this.hasMyBuildTagInTTPComment(content, buildKeyPart); 360 | } 361 | rebuildCommentAvailable &= !hasMyBuildTag; 362 | } 363 | if (rebuildCommentAvailable) this.postBuildTagInTTPComment(id, "TTP build flag", buildKeyPart); 364 | 365 | final boolean canBuildTarget = rebuildCommentAvailable || !commitAlreadyBeenProcessed; 366 | logger.log(Level.FINE, "Build target? {0} [rebuild:{1} processed:{2}]", new Object[]{ canBuildTarget, rebuildCommentAvailable, commitAlreadyBeenProcessed}); 367 | return canBuildTarget; 368 | } 369 | 370 | return false; 371 | } 372 | 373 | private boolean isSkipBuild(String pullRequestTitle) { 374 | String skipPhrases = this.trigger.getCiSkipPhrases(); 375 | if (skipPhrases != null && !"".equals(skipPhrases)) { 376 | String[] phrases = skipPhrases.split(","); 377 | for(String phrase : phrases) { 378 | if (pullRequestTitle.toLowerCase().contains(phrase.trim().toLowerCase())) { 379 | return true; 380 | } 381 | } 382 | } 383 | return false; 384 | } 385 | 386 | private boolean isFilteredBuild(AbstractPullrequest pullRequest) { 387 | 388 | BitbucketCause cause = createCause(pullRequest); 389 | 390 | //@FIXME: Way to iterate over all available SCMSources 391 | List sources = new LinkedList(); 392 | for(SCMSourceOwner owner : SCMSourceOwners.all()) 393 | for(SCMSource src : owner.getSCMSources()) 394 | sources.add(src); 395 | 396 | BitbucketBuildFilter filter = !this.trigger.getBranchesFilterBySCMIncludes() ? 397 | BitbucketBuildFilter.instanceByString(this.trigger.getBranchesFilter()) : 398 | BitbucketBuildFilter.instanceBySCM(sources, this.trigger.getBranchesFilter()); 399 | 400 | return filter.approved(cause); 401 | } 402 | 403 | private StandardUsernamePasswordCredentials getCredentials(String credentialsId) { 404 | if (null == credentialsId) return null; 405 | return CredentialsMatchers 406 | .firstOrNull( 407 | CredentialsProvider.lookupCredentials( 408 | StandardUsernamePasswordCredentials.class, 409 | (Item) null, 410 | ACL.SYSTEM, 411 | (DomainRequirement) null 412 | ), 413 | CredentialsMatchers.allOf(CredentialsMatchers.withId(credentialsId), instanceOf(UsernamePasswordCredentials.class)) 414 | ); 415 | } 416 | } 417 | -------------------------------------------------------------------------------- /src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/AbstractPullrequest.java: -------------------------------------------------------------------------------- 1 | package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket; 2 | 3 | import org.codehaus.jackson.annotate.JsonIgnoreProperties; 4 | import org.codehaus.jackson.annotate.JsonProperty; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public abstract class AbstractPullrequest { 10 | 11 | protected static final String AUTHOR_COMBINED_NAME = "%s <@%s>"; 12 | 13 | public interface Revision { 14 | Repository getRepository(); 15 | 16 | Branch getBranch(); 17 | 18 | Commit getCommit(); 19 | } 20 | 21 | public interface Repository { 22 | String getName(); 23 | 24 | String getOwnerName(); 25 | 26 | String getRepositoryName(); 27 | 28 | RepositoryLinks getLinks(); 29 | } 30 | 31 | /** 32 | * Represents various URIs connected to the repository (e.g. git clone URIs, various BitBucket repository web 33 | * URIs, etc.). 34 | * 35 | * Used to provide more specific target git revision information (git repository URI) when scheduling 36 | * Jenkins builds. 37 | */ 38 | @JsonIgnoreProperties(ignoreUnknown = true) 39 | public static class RepositoryLinks { 40 | /** 41 | * List of URIs that can be used for the git clone operation. 42 | */ 43 | private List cloneLinks = new ArrayList<>(); 44 | 45 | @JsonProperty("clone") 46 | public void setCloneLinks(List cloneLinks) { 47 | this.cloneLinks = cloneLinks; 48 | } 49 | 50 | @JsonProperty("clone") 51 | public List getCloneLinks() { 52 | return cloneLinks; 53 | } 54 | } 55 | 56 | /** 57 | * Repository URI (e.g. a git clone URI, BitBucket repository web URI, etc.) 58 | */ 59 | @JsonIgnoreProperties(ignoreUnknown = true) 60 | public static class RepositoryLink { 61 | /** 62 | * Used to distinguish between various git clone URIs (e.g. https or ssh). Mostly unused otherwise. 63 | */ 64 | private String name; 65 | 66 | /** 67 | * Actual URI value. 68 | */ 69 | private String href; 70 | 71 | public void setName(String name) { 72 | this.name = name; 73 | } 74 | public void setHref(String href) { 75 | this.href = href; 76 | } 77 | public String getName() { 78 | return name; 79 | } 80 | public String getHref() { 81 | return href; 82 | } 83 | } 84 | 85 | public interface Branch { 86 | String getName(); 87 | } 88 | 89 | public interface Commit { 90 | String getHash(); 91 | } 92 | 93 | public interface Author { 94 | String getUsername(); 95 | 96 | String getDisplayName(); 97 | 98 | String getCombinedUsername(); 99 | } 100 | 101 | public interface Participant { 102 | String getRole() ; 103 | 104 | Boolean getApproved(); 105 | } 106 | 107 | public interface Comment extends Comparable { 108 | Integer getId(); 109 | 110 | String getContent(); 111 | } 112 | 113 | @JsonIgnoreProperties(ignoreUnknown = true) 114 | public static class Response { 115 | private int pageLength; 116 | private List values; 117 | private int page; 118 | private int size; 119 | private String next; 120 | 121 | @JsonProperty("pagelen") 122 | public int getPageLength() { 123 | return pageLength; 124 | } 125 | @JsonProperty("pagelen") 126 | public void setPageLength(int pageLength) { 127 | this.pageLength = pageLength; 128 | } 129 | public List getValues() { 130 | return values; 131 | } 132 | public void setValues(List values) { 133 | this.values = values; 134 | } 135 | public int getPage() { 136 | return page; 137 | } 138 | public void setPage(int page) { 139 | this.page = page; 140 | } 141 | public int getSize() { 142 | return size; 143 | } 144 | public void setSize(int size) { 145 | this.size = size; 146 | } 147 | public String getNext() { 148 | return next; 149 | } 150 | public void setNext(String next) { 151 | this.next = next; 152 | } 153 | } 154 | 155 | public abstract String getTitle(); 156 | 157 | public abstract Revision getDestination(); 158 | 159 | public abstract Revision getSource(); 160 | 161 | public abstract String getState(); 162 | 163 | public abstract String getId(); 164 | 165 | public abstract Author getAuthor(); 166 | } -------------------------------------------------------------------------------- /src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/ApiClient.java: -------------------------------------------------------------------------------- 1 | package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket; 2 | 3 | import org.apache.commons.codec.binary.Hex; 4 | import org.apache.commons.httpclient.*; 5 | import org.apache.commons.httpclient.auth.AuthScope; 6 | import org.apache.commons.httpclient.methods.*; 7 | import org.apache.commons.httpclient.methods.DeleteMethod; 8 | import org.apache.commons.httpclient.params.HttpClientParams; 9 | import org.apache.commons.httpclient.util.EncodingUtil; 10 | import org.apache.commons.io.IOUtils; 11 | import org.codehaus.jackson.map.ObjectMapper; 12 | import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; 13 | import org.codehaus.jackson.type.JavaType; 14 | import org.codehaus.jackson.type.TypeReference; 15 | 16 | import java.io.IOException; 17 | import java.io.UnsupportedEncodingException; 18 | import java.security.MessageDigest; 19 | import java.security.NoSuchAlgorithmException; 20 | import java.util.List; 21 | import java.util.logging.Level; 22 | import java.util.logging.Logger; 23 | 24 | import jenkins.model.Jenkins; 25 | import hudson.ProxyConfiguration; 26 | 27 | /** 28 | * Created by nishio 29 | */ 30 | public abstract class ApiClient { 31 | private static final Logger logger = Logger.getLogger(ApiClient.class.getName()); 32 | 33 | private static final String COMPUTED_KEY_FORMAT = "%s-%s"; 34 | public static final byte MAX_KEY_SIZE_BB_API = 40; 35 | 36 | protected String owner; 37 | protected String repositoryName; 38 | protected Credentials credentials; 39 | protected String key; 40 | protected String name; 41 | protected HttpClientFactory factory; 42 | 43 | private static MessageDigest SHA1 = null; 44 | 45 | public static class HttpClientFactory { 46 | public static final HttpClientFactory INSTANCE = new HttpClientFactory(); 47 | private static final int DEFAULT_TIMEOUT = 60000; 48 | 49 | public HttpClient getInstanceHttpClient() { 50 | HttpClient client = new HttpClient(); 51 | 52 | HttpClientParams params = client.getParams(); 53 | params.setConnectionManagerTimeout(DEFAULT_TIMEOUT); 54 | params.setSoTimeout(DEFAULT_TIMEOUT); 55 | 56 | if (Jenkins.getInstance() == null) return client; 57 | 58 | ProxyConfiguration proxy = getInstance().proxy; 59 | if (proxy == null) return client; 60 | 61 | logger.log(Level.FINE, "Jenkins proxy: {0}:{1}", new Object[]{ proxy.name, proxy.port }); 62 | client.getHostConfiguration().setProxy(proxy.name, proxy.port); 63 | String username = proxy.getUserName(); 64 | String password = proxy.getPassword(); 65 | 66 | // Consider it to be passed if username specified. Sufficient? 67 | if (username != null && !"".equals(username.trim())) { 68 | logger.log(Level.FINE, "Using proxy authentication (user={0})", username); 69 | client.getState().setProxyCredentials(AuthScope.ANY, 70 | new UsernamePasswordCredentials(username, password)); 71 | } 72 | 73 | return client; 74 | } 75 | 76 | private Jenkins getInstance() { 77 | final Jenkins instance = Jenkins.getInstance(); 78 | if (instance == null){ 79 | throw new IllegalStateException("Jenkins instance is NULL!"); 80 | } 81 | return instance; 82 | } 83 | } 84 | 85 | public ApiClient( 86 | String username, String password, 87 | String owner, String repositoryName, 88 | String key, String name, 89 | T httpFactory 90 | ) { 91 | this.credentials = new UsernamePasswordCredentials(username, password); 92 | this.owner = owner; 93 | this.repositoryName = repositoryName; 94 | this.key = key; 95 | this.name = name; 96 | this.factory = httpFactory != null ? httpFactory : HttpClientFactory.INSTANCE; 97 | } 98 | 99 | /** 100 | * Retrun 101 | * @param keyExPart 102 | * @return key parameter for call BitBucket API 103 | */ 104 | protected String computeAPIKey(String keyExPart) { 105 | String computedKey = String.format(COMPUTED_KEY_FORMAT, this.key, keyExPart); 106 | 107 | if (computedKey.length() > MAX_KEY_SIZE_BB_API) { 108 | try { 109 | if (SHA1 == null) SHA1 = MessageDigest.getInstance("SHA1"); 110 | return new String(Hex.encodeHex(SHA1.digest(computedKey.getBytes("UTF-8")))); 111 | } catch(NoSuchAlgorithmException e) { 112 | logger.log(Level.WARNING, "Failed to create hash provider", e); 113 | } catch (UnsupportedEncodingException e) { 114 | logger.log(Level.WARNING, "Failed to create hash provider", e); 115 | } 116 | } 117 | return (computedKey.length() <= MAX_KEY_SIZE_BB_API) ? computedKey : computedKey.substring(0, MAX_KEY_SIZE_BB_API); 118 | } 119 | 120 | private HttpClient getHttpClient() { 121 | return this.factory.getInstanceHttpClient(); 122 | } 123 | 124 | protected String get(String path) { 125 | return send(new GetMethod(path)); 126 | } 127 | 128 | protected String post(String path, NameValuePair[] data) { 129 | PostMethod req = new PostMethod(path); 130 | req.setRequestBody(data); 131 | req.getParams().setContentCharset("utf-8"); 132 | return send(req); 133 | } 134 | 135 | // Public static JSON serializer, so we can test serialization 136 | public static String serializeObject(Object obj) throws java.io.IOException { 137 | String jsonStr = new ObjectMapper(). 138 | setSerializationInclusion(Inclusion.NON_NULL). 139 | writeValueAsString(obj); 140 | return jsonStr; 141 | } 142 | 143 | protected String post(String path, Object data) { 144 | try { 145 | final String jsonStr = ApiClient.serializeObject(data); 146 | final StringRequestEntity entity = new StringRequestEntity(jsonStr, "application/json", "utf-8"); 147 | PostMethod req = new PostMethod(path); 148 | req.setRequestEntity(entity); 149 | logger.log(Level.FINE, "SENDING:\n" + jsonStr + "\n"); 150 | return send(req); 151 | } catch (IOException e) { 152 | logger.log(Level.WARNING, "Not able to parse data to json", e); 153 | } 154 | return null; 155 | } 156 | 157 | protected String post(String path) { 158 | final PostMethod req = new PostMethod(path); 159 | req.setRequestHeader("X-Atlassian-Token", "no-check"); 160 | return send(req); 161 | } 162 | 163 | protected void delete(String path) { 164 | send(new DeleteMethod(path)); 165 | } 166 | 167 | protected void put(String path, NameValuePair[] data) { 168 | PutMethod req = new PutMethod(path); 169 | req.setRequestBody(EncodingUtil.formUrlEncode(data, "utf-8")); 170 | req.getParams().setContentCharset("utf-8"); 171 | send(req); 172 | } 173 | 174 | private String send(HttpMethodBase req) { 175 | HttpClient client = getHttpClient(); 176 | client.getState().setCredentials(AuthScope.ANY, credentials); 177 | client.getParams().setAuthenticationPreemptive(true); 178 | try { 179 | int statusCode = client.executeMethod(req); 180 | if (statusCode == HttpStatus.SC_NO_CONTENT) { 181 | // Empty 182 | return null; 183 | 184 | // Not sure if We should list the success codes, or check for < 200 and > 207 . . . 185 | // I kind of prefer listing the ones we expect. 186 | } else if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_ACCEPTED || 187 | statusCode == HttpStatus.SC_CREATED) { 188 | // Success! 189 | return IOUtils.toString(req.getResponseBodyAsStream()); 190 | 191 | } else { 192 | // Bad response status 193 | logger.log(Level.WARNING, "Response status: " + req.getStatusLine()+" URI: "+req.getURI()); 194 | logger.log(Level.WARNING, IOUtils.toString(req.getResponseBodyAsStream())); 195 | } 196 | } catch (HttpException e) { 197 | logger.log(Level.WARNING, "Failed to send request.", e); 198 | } catch (IOException e) { 199 | logger.log(Level.WARNING, "Failed to send request.", e); 200 | } finally { 201 | req.releaseConnection(); 202 | } 203 | return null; 204 | } 205 | 206 | protected R parse(String response, Class cls) throws IOException { 207 | return new ObjectMapper().readValue(response, cls); 208 | } 209 | protected R parse(String response, JavaType type) throws IOException { 210 | return new ObjectMapper().readValue(response, type); 211 | } 212 | protected R parse(String response, TypeReference ref) throws IOException { 213 | return new ObjectMapper().readValue(response, ref); 214 | } 215 | 216 | public abstract List getPullRequests(); 217 | 218 | public abstract List getPullRequestComments(String commentOwnerName, String commentRepositoryName, String pullRequestId); 219 | 220 | public String buildStatusKey(String bsKey) { 221 | return this.computeAPIKey(bsKey); 222 | } 223 | 224 | public abstract boolean hasBuildStatus(String owner, String repositoryName, String revision, String keyEx); 225 | 226 | public abstract void setBuildStatus(String owner, String repositoryName, String revision, BuildState state, String buildUrl, String comment, String keyEx); 227 | 228 | public abstract void deletePullRequestApproval(String pullRequestId); 229 | 230 | public abstract AbstractPullrequest.Participant postPullRequestApproval(String pullRequestId); 231 | 232 | public abstract AbstractPullrequest.Comment postPullRequestComment(String pullRequestId, String content); 233 | 234 | public String getName() { 235 | return this.name; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/BuildState.java: -------------------------------------------------------------------------------- 1 | package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket; 2 | 3 | /** 4 | * Valid build states for a pull request 5 | * 6 | * @see "https://confluence.atlassian.com/bitbucket/buildstatus-resource-779295267.html" 7 | */ 8 | public enum BuildState { 9 | FAILED, INPROGRESS, SUCCESSFUL 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/cloud/CloudApiClient.java: -------------------------------------------------------------------------------- 1 | package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.cloud; 2 | 3 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.AbstractPullrequest; 4 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.ApiClient; 5 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.BuildState; 6 | import org.apache.commons.httpclient.NameValuePair; 7 | import org.codehaus.jackson.map.type.TypeFactory; 8 | import org.codehaus.jackson.type.JavaType; 9 | import org.codehaus.jackson.type.TypeReference; 10 | 11 | import java.io.IOException; 12 | import java.net.URLEncoder; 13 | import java.nio.charset.StandardCharsets; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | import java.util.logging.Level; 17 | import java.util.logging.Logger; 18 | 19 | public class CloudApiClient extends ApiClient { 20 | 21 | private static final Logger logger = Logger.getLogger(CloudApiClient.class.getName()); 22 | 23 | private static final String V2_API_BASE_URL = "https://bitbucket.org/api/2.0/repositories/"; 24 | 25 | public CloudApiClient(String username, String password, String owner, String repositoryName, String key, String name, T httpFactory) { 26 | super(username, password, owner, repositoryName, key, name, httpFactory); 27 | } 28 | 29 | @Override 30 | public List getPullRequests() { 31 | return getAllValues( 32 | v2("/pullrequests/"), 33 | 50, 34 | "+values.source.repository.links.clone.*", 35 | CloudPullrequest.class 36 | ); 37 | } 38 | 39 | @Override 40 | public List getPullRequestComments(String commentOwnerName, String commentRepositoryName, String pullRequestId) { 41 | 42 | final List comments = getAllValues(v2("/pullrequests/" + pullRequestId + "/comments"), 100, null, CloudPullrequest.Comment.class); 43 | return cloudToAbstractComments(comments); 44 | } 45 | 46 | private List cloudToAbstractComments(List comments) { 47 | // There has got to be a better way to do this? 48 | // Sorry - my java OO-fu is weak. 49 | 50 | final List resultComments = new ArrayList<>(); 51 | for (final CloudPullrequest.Comment comment : comments) { 52 | resultComments.add(comment); 53 | } 54 | 55 | return resultComments; 56 | } 57 | 58 | @Override 59 | public boolean hasBuildStatus(String owner, String repositoryName, String revision, String keyEx) { 60 | String url = v2(owner, repositoryName, "/commit/" + revision + "/statuses/build/" + computeAPIKey(keyEx)); 61 | String reqBody = get(url); 62 | logger.log(Level.FINE, "hasBuildStatus response: " + reqBody); 63 | return reqBody != null && reqBody.contains("\"state\""); 64 | } 65 | 66 | @Override 67 | public void setBuildStatus(String owner, String repositoryName, String revision, BuildState state, String buildUrl, String comment, String keyEx) { 68 | String url = v2(owner, repositoryName, "/commit/" + revision + "/statuses/build"); 69 | String computedKey = this.computeAPIKey(keyEx); 70 | 71 | NameValuePair[] data = new NameValuePair[]{ 72 | new NameValuePair("description", comment), 73 | new NameValuePair("key", computedKey), 74 | new NameValuePair("name", this.name), 75 | new NameValuePair("state", state.toString()), 76 | new NameValuePair("url", buildUrl), 77 | }; 78 | 79 | String resp = post(url, data); 80 | 81 | logger.log(Level.FINE, "POST state {0} to {1} with key {2} with response {3}", new Object[]{ 82 | state, url, computedKey, resp} 83 | ); 84 | } 85 | 86 | @Override 87 | public void deletePullRequestApproval(String pullRequestId) { 88 | delete(v2("/pullrequests/" + pullRequestId + "/approve")); 89 | } 90 | 91 | public void deletePullRequestComment(String pullRequestId, String commentId) { 92 | delete(v2("/pullrequests/" + pullRequestId + "/comments/" + commentId)); 93 | } 94 | 95 | public void updatePullRequestComment(String pullRequestId, String content, String commentId) { 96 | NameValuePair[] data = new NameValuePair[] { 97 | new NameValuePair("content", content), 98 | }; 99 | put(v2("/pullrequests/" + pullRequestId + "/comments/" + commentId), data); 100 | } 101 | 102 | @Override 103 | public AbstractPullrequest.Participant postPullRequestApproval(String pullRequestId) { 104 | try { 105 | return parse(post(v2("/pullrequests/" + pullRequestId + "/approve"), 106 | new NameValuePair[]{}), CloudPullrequest.Participant.class); 107 | } catch (IOException e) { 108 | logger.log(Level.WARNING, "Invalid pull request approval response.", e); 109 | } 110 | return null; 111 | } 112 | 113 | @Override 114 | public AbstractPullrequest.Comment postPullRequestComment(String pullRequestId, String content) { 115 | CloudPullrequest.Comment data = new CloudPullrequest.Comment(content); 116 | try { 117 | String response = post(v2("/pullrequests/" + pullRequestId + "/comments"), data); 118 | logger.log(Level.FINE, "postCommentResponse: " + response); 119 | return parse(response, new TypeReference() {}); 120 | } catch(Exception e) { 121 | logger.log(Level.WARNING, "Invalid pull request comment response.", e); 122 | } 123 | return null; 124 | } 125 | 126 | private String v2(String path) { 127 | return v2(this.owner, this.repositoryName, path); 128 | } 129 | 130 | private String v2(String owner, String repositoryName, String path) { 131 | return V2_API_BASE_URL + owner + "/" + repositoryName + path; 132 | } 133 | 134 | private List getAllValues(String rootUrl, int pageLen, String additionalFieldsSpec, Class cls) { 135 | List values = new ArrayList(); 136 | try { 137 | String url = rootUrl + "?pagelen=" + pageLen; 138 | if (additionalFieldsSpec != null) { 139 | url += "&fields=" + URLEncoder.encode(additionalFieldsSpec, StandardCharsets.UTF_8.toString()); 140 | } 141 | do { 142 | final JavaType type = TypeFactory.defaultInstance().constructParametricType(AbstractPullrequest.Response.class, cls); 143 | final String body = get(url); 144 | logger.log(Level.FINE, "****Received("+url+")****:\n" + body + "\n"); 145 | AbstractPullrequest.Response response = parse(body, type); 146 | values.addAll(response.getValues()); 147 | url = response.getNext(); 148 | } while (url != null); 149 | } catch (Exception e) { 150 | logger.log(Level.WARNING, "invalid response.", e); 151 | } 152 | return values; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/cloud/CloudBitbucketCause.java: -------------------------------------------------------------------------------- 1 | package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.cloud; 2 | 3 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketCause; 4 | 5 | /** 6 | * Created by nishio 7 | */ 8 | public class CloudBitbucketCause extends BitbucketCause { 9 | 10 | public static final String BITBUCKET_URL = "https://bitbucket.org/"; 11 | 12 | public CloudBitbucketCause(String sourceBranch, 13 | String targetBranch, 14 | String repositoryOwner, 15 | String repositoryName, 16 | String repositoryUri, 17 | String pullRequestId, 18 | String destinationRepositoryOwner, 19 | String destinationRepositoryName, 20 | String pullRequestTitle, 21 | String sourceCommitHash, 22 | String destinationCommitHash, 23 | String pullRequestAuthor) { 24 | super(sourceBranch, targetBranch, repositoryOwner, repositoryName, repositoryUri, pullRequestId, 25 | destinationRepositoryOwner, destinationRepositoryName, pullRequestTitle, sourceCommitHash, 26 | destinationCommitHash, pullRequestAuthor); 27 | } 28 | 29 | @Override 30 | public String getShortDescription() { 31 | String description = "#" + this.getPullRequestId() + " " + this.getPullRequestTitle() + ""; 34 | return description; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/cloud/CloudPullrequest.java: -------------------------------------------------------------------------------- 1 | package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.cloud; 2 | 3 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.AbstractPullrequest; 4 | import org.codehaus.jackson.annotate.JsonIgnore; 5 | import org.codehaus.jackson.annotate.JsonIgnoreProperties; 6 | import org.codehaus.jackson.annotate.JsonProperty; 7 | 8 | /** 9 | * POJOs representing the pull-requests extracted from the 10 | * JSON response of the Bitbucket API V2. 11 | * 12 | * @see "https://confluence.atlassian.com/bitbucket/pullrequests-resource-423626332.html#pullrequestsResource-GETaspecificpullrequest" 13 | */ 14 | 15 | @JsonIgnoreProperties(ignoreUnknown = true) 16 | public class CloudPullrequest extends AbstractPullrequest { 17 | 18 | private String description; 19 | private Boolean closeSourceBranch; 20 | private String title; 21 | private Revision destination; 22 | private String reason; 23 | private String closedBy; 24 | private Revision source; 25 | private String state; 26 | private String createdOn; 27 | private String updatedOn; 28 | private String mergeCommit; 29 | private String id; 30 | private Author author; 31 | 32 | @JsonIgnoreProperties(ignoreUnknown = true) 33 | public static class Revision implements AbstractPullrequest.Revision { 34 | private Repository repository; 35 | private Branch branch; 36 | private Commit commit; 37 | 38 | public Repository getRepository() { 39 | return repository; 40 | } 41 | public void setRepository(Repository repository) { 42 | this.repository = repository; 43 | } 44 | public Branch getBranch() { 45 | return branch; 46 | } 47 | public void setBranch(Branch branch) { 48 | this.branch = branch; 49 | } 50 | public Commit getCommit() { 51 | return commit; 52 | } 53 | public void setCommit(Commit commit) { 54 | this.commit = commit; 55 | } 56 | } 57 | 58 | @JsonIgnoreProperties(ignoreUnknown = true) 59 | public static class Repository implements AbstractPullrequest.Repository { 60 | private String fullName; 61 | private String name; 62 | private String ownerName; 63 | private String repositoryName; 64 | private RepositoryLinks links; 65 | 66 | @JsonProperty("full_name") 67 | public String getFullName() { 68 | return fullName; 69 | } 70 | @JsonProperty("full_name") 71 | public void setFullName(String fullName) { 72 | // Also extract owner- and reponame 73 | if (fullName != null) { 74 | this.ownerName = fullName.split("/")[0]; 75 | this.repositoryName = fullName.split("/")[1]; 76 | } 77 | this.fullName = fullName; 78 | } 79 | public String getName() { 80 | return name; 81 | } 82 | public void setName(String name) { 83 | this.name = name; 84 | } 85 | public void setLinks(RepositoryLinks links) { 86 | this.links = links; 87 | } 88 | @Override public String getOwnerName() { 89 | return ownerName; 90 | } 91 | @Override public String getRepositoryName() { 92 | return repositoryName; 93 | } 94 | @Override public RepositoryLinks getLinks() { 95 | return links; 96 | } 97 | } 98 | 99 | @JsonIgnoreProperties(ignoreUnknown = true) 100 | public static class Branch implements AbstractPullrequest.Branch { 101 | private String name; 102 | 103 | public String getName() { 104 | return name; 105 | } 106 | 107 | public void setName(String name) { 108 | this.name = name; 109 | } 110 | } 111 | 112 | @JsonIgnoreProperties(ignoreUnknown = true) 113 | public static class Commit implements AbstractPullrequest.Commit { 114 | private String hash; 115 | 116 | public String getHash() { 117 | return hash; 118 | } 119 | 120 | public void setHash(String hash) { 121 | this.hash = hash; 122 | } 123 | } 124 | 125 | // Was: Approval 126 | @JsonIgnoreProperties(ignoreUnknown = true) 127 | public static class Participant implements AbstractPullrequest.Participant { 128 | private String role; 129 | private Boolean approved; 130 | 131 | public String getRole() { 132 | return role; 133 | } 134 | public void setRole(String role) { 135 | this.role = role; 136 | } 137 | public Boolean getApproved() { 138 | return approved; 139 | } 140 | public void setApproved(Boolean approved) { 141 | this.approved = approved; 142 | } 143 | } 144 | 145 | @JsonIgnoreProperties(ignoreUnknown = true) 146 | public static class Author implements AbstractPullrequest.Author { 147 | private String username; 148 | private String display_name; 149 | 150 | public String getUsername() { 151 | return username; 152 | } 153 | public void setUsername(String username) { 154 | this.username = username; 155 | } 156 | 157 | @JsonProperty("display_name") 158 | public String getDisplayName() { 159 | return display_name; 160 | } 161 | 162 | @JsonProperty("display_name") 163 | public void setDisplayName(String display_name) { 164 | this.display_name = display_name; 165 | } 166 | public String getCombinedUsername() { 167 | return String.format(AUTHOR_COMBINED_NAME, this.getDisplayName(), this.getUsername()); 168 | } 169 | } 170 | 171 | // https://confluence.atlassian.com/bitbucket/pullrequests-resource-1-0-296095210.html#pullrequestsResource1.0-POSTanewcomment 172 | @JsonIgnoreProperties(ignoreUnknown = true) 173 | public static class Comment implements AbstractPullrequest.Comment { 174 | private Integer id; 175 | private Content content; 176 | 177 | public Comment() { 178 | } 179 | 180 | public Comment(String rawContent) { 181 | this.content = new Content(); 182 | this.content.setRaw(rawContent); 183 | } 184 | 185 | @JsonIgnoreProperties(ignoreUnknown = true) 186 | public static class Content { 187 | private String raw; 188 | 189 | public String getRaw() { 190 | return raw; 191 | } 192 | 193 | public void setRaw(String rawContent) { 194 | this.raw = rawContent; 195 | } 196 | } 197 | 198 | @Override 199 | public int compareTo(AbstractPullrequest.Comment target) { 200 | if (target == null){ 201 | return -1; 202 | } else if (this.getId() > target.getId()) { 203 | return 1; 204 | } else if (this.getId().equals(target.getId())) { 205 | return 0; 206 | } else { 207 | return -1; 208 | } 209 | } 210 | 211 | @Override 212 | public boolean equals(final Object o) { 213 | if (this == o) return true; 214 | if (o == null || getClass() != o.getClass()) return false; 215 | 216 | final Comment comment = (Comment) o; 217 | 218 | return getId() != null ? getId().equals(comment.getId()) : comment.getId() == null; 219 | } 220 | 221 | @Override 222 | public int hashCode() { 223 | return getId() != null ? getId().hashCode() : 0; 224 | } 225 | 226 | public Integer getId() { 227 | return id; 228 | } 229 | 230 | public void setId(Integer id) { 231 | this.id = id; 232 | } 233 | 234 | // This annotation prevents getContent() - the abstract method, from being used in json 235 | // serialization/deserialization 236 | @JsonIgnore 237 | public String getContent() { 238 | if (content == null) { 239 | return ""; 240 | } 241 | return content.getRaw(); 242 | } 243 | 244 | // This annotation is needed so that the serializer will use the actual content 245 | // for serialization, even though the abstract class assumes getContent() will return a string. 246 | @JsonProperty("content") 247 | public Content getContentRaw() { 248 | return content; 249 | } 250 | 251 | // And, since the getContent didn't get grabbed by default due to my @JsonIgnore, we need to 252 | // tell it setContent is still ok to use. 253 | @JsonProperty("content") 254 | public void setContent(Content content) { 255 | this.content = content; 256 | } 257 | } 258 | 259 | //-------------------- only getters and setters follow ----------------- 260 | 261 | public String getDescription() { 262 | return description; 263 | } 264 | 265 | public void setDescription(String description) { 266 | this.description = description; 267 | } 268 | 269 | @JsonProperty("close_source_branch") 270 | public Boolean getCloseSourceBranch() { 271 | return closeSourceBranch; 272 | } 273 | 274 | @JsonProperty("close_source_branch") 275 | public void setCloseSourceBranch(Boolean closeSourceBranch) { 276 | this.closeSourceBranch = closeSourceBranch; 277 | } 278 | 279 | public String getTitle() { 280 | return title; 281 | } 282 | 283 | public void setTitle(String title) { 284 | this.title = title; 285 | } 286 | 287 | public Revision getDestination() { 288 | return destination; 289 | } 290 | 291 | public void setDestination(Revision destination) { 292 | this.destination = destination; 293 | } 294 | 295 | public String getReason() { 296 | return reason; 297 | } 298 | 299 | public void setReason(String reason) { 300 | this.reason = reason; 301 | } 302 | 303 | @JsonProperty("closed_by") 304 | public String getClosedBy() { 305 | return closedBy; 306 | } 307 | 308 | @JsonProperty("closed_by") 309 | public void setClosedBy(String closedBy) { 310 | this.closedBy = closedBy; 311 | } 312 | 313 | public Revision getSource() { 314 | return source; 315 | } 316 | 317 | public void setSource(Revision source) { 318 | this.source = source; 319 | } 320 | 321 | public String getState() { 322 | return state; 323 | } 324 | 325 | public void setState(String state) { 326 | this.state = state; 327 | } 328 | 329 | @JsonProperty("created_on") 330 | public String getCreatedOn() { 331 | return createdOn; 332 | } 333 | 334 | @JsonProperty("created_on") 335 | public void setCreatedOn(String createdOn) { 336 | this.createdOn = createdOn; 337 | } 338 | 339 | @JsonProperty("updated_on") 340 | public String getUpdatedOn() { 341 | return updatedOn; 342 | } 343 | 344 | @JsonProperty("updated_on") 345 | public void setUpdatedOn(String updatedOn) { 346 | this.updatedOn = updatedOn; 347 | } 348 | 349 | @JsonProperty("merge_commit") 350 | public String getMergeCommit() { 351 | return mergeCommit; 352 | } 353 | 354 | @JsonProperty("merge_commit") 355 | public void setMergeCommit(String mergeCommit) { 356 | this.mergeCommit = mergeCommit; 357 | } 358 | 359 | public String getId() { 360 | return id; 361 | } 362 | 363 | public void setId(String id) { 364 | this.id = id; 365 | } 366 | 367 | public Author getAuthor() { 368 | return this.author; 369 | } 370 | 371 | public void setAuthor(Author author) { 372 | this.author = author; 373 | } 374 | 375 | } 376 | -------------------------------------------------------------------------------- /src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/server/ServerApiClient.java: -------------------------------------------------------------------------------- 1 | package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.server; 2 | 3 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.AbstractPullrequest; 4 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.ApiClient; 5 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.BuildState; 6 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.cloud.CloudApiClient; 7 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.cloud.CloudPullrequest; 8 | import org.apache.commons.collections.CollectionUtils; 9 | import org.apache.commons.httpclient.NameValuePair; 10 | import org.codehaus.jackson.map.type.TypeFactory; 11 | import org.codehaus.jackson.type.JavaType; 12 | import org.codehaus.jackson.type.TypeReference; 13 | 14 | import java.io.IOException; 15 | import java.util.ArrayList; 16 | import java.util.Collections; 17 | import java.util.List; 18 | import java.util.logging.Level; 19 | import java.util.logging.Logger; 20 | 21 | public class ServerApiClient extends ApiClient { 22 | 23 | private static final Logger logger = Logger.getLogger(CloudApiClient.class.getName()); 24 | 25 | private static final String PULL_REQUESTS_URL = "/pull-requests/"; 26 | 27 | private final String serverUrl; 28 | 29 | public ServerApiClient(String serverUrl, String username, String password, String owner, String repositoryName, String key, String name, T httpFactory) { 30 | super(username, password, owner, repositoryName, key, name, httpFactory); 31 | 32 | this.serverUrl = serverUrl; 33 | } 34 | 35 | @Override 36 | public List getPullRequests() { 37 | return getAllValues(restV1(PULL_REQUESTS_URL), ServerPullrequest.class); 38 | } 39 | 40 | @Override 41 | public List getPullRequestComments(String commentOwnerName, String commentRepositoryName, String pullRequestId) { 42 | final List activities = getAllValues(restV1(PULL_REQUESTS_URL + pullRequestId + "/activities"), ServerPullrequest.Activity.class); 43 | return activitiesToComments(activities); 44 | } 45 | 46 | private List activitiesToComments(List activities) { 47 | if (CollectionUtils.isEmpty(activities)) 48 | return Collections.emptyList(); 49 | 50 | final List comments = new ArrayList<>(); 51 | for (final ServerPullrequest.Activity activity : activities) { 52 | if (activity.isComment()) { 53 | comments.add(activity.toComment()); 54 | } 55 | } 56 | 57 | return comments; 58 | } 59 | 60 | @Override 61 | public boolean hasBuildStatus(String owner, String repositoryName, String revision, String keyEx) { 62 | return CollectionUtils.isNotEmpty(getAllValues(buildStateV1(revision), ServerPullrequest.CommitBuildState.class)); 63 | } 64 | 65 | @Override 66 | public void setBuildStatus(String owner, String repositoryName, String revision, BuildState state, String buildUrl, String comment, String keyEx) { 67 | String url = buildStateV1(revision); 68 | String computedKey = computeAPIKey(keyEx); 69 | ServerPullrequest.CommitBuildState commit = new ServerPullrequest.CommitBuildState(); 70 | 71 | commit.setName(this.name); 72 | commit.setDescription(comment); 73 | commit.setKey(computedKey); 74 | commit.setState(state); 75 | commit.setUrl(buildUrl); 76 | 77 | String resp = post(url, commit); 78 | 79 | logger.log(Level.FINE, "POST state {0} to {1} with key {2} with response {3}", new Object[]{ 80 | state, url, computedKey, resp} 81 | ); 82 | } 83 | 84 | @Override 85 | public void deletePullRequestApproval(String pullRequestId) { 86 | delete(restV1(PULL_REQUESTS_URL + pullRequestId + "/approve")); 87 | } 88 | 89 | @Override 90 | public CloudPullrequest.Participant postPullRequestApproval(String pullRequestId) { 91 | post(restV1(PULL_REQUESTS_URL + pullRequestId + "/approve")); 92 | return null; 93 | } 94 | 95 | @Override 96 | public ServerPullrequest.Comment postPullRequestComment(String pullRequestId, String content) { 97 | ServerPullrequest.Comment comment = new ServerPullrequest.Comment(); 98 | comment.setContent(content); 99 | try { 100 | post(restV1(PULL_REQUESTS_URL + pullRequestId + "/comments"), comment); 101 | } catch(Exception e) { 102 | logger.log(Level.WARNING, "Invalid pull request comment response.", e); 103 | } 104 | return comment; 105 | } 106 | 107 | private String restV1(String url) { 108 | return this.serverUrl + "/rest/api/1.0/projects/" + this.owner + "/repos/" + this.repositoryName + url; 109 | } 110 | 111 | private String buildStateV1(String commit) { 112 | return this.serverUrl + "/rest/build-status/1.0/commits/" + commit; 113 | } 114 | 115 | private List getAllValues(String rootUrl, Class cls) { 116 | List values = new ArrayList<>(); 117 | try { 118 | String url = rootUrl; 119 | do { 120 | final JavaType type = TypeFactory.defaultInstance().constructParametricType(AbstractPullrequest.Response.class, cls); 121 | final String respBody = get(url); 122 | AbstractPullrequest.Response response = parse(respBody, type); 123 | values.addAll(response.getValues()); 124 | url = response.getNext(); 125 | } while (url != null); 126 | } catch (Exception e) { 127 | logger.log(Level.WARNING, "invalid response.", e); 128 | } 129 | return values; 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/server/ServerBitbucketCause.java: -------------------------------------------------------------------------------- 1 | package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.server; 2 | 3 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketCause; 4 | 5 | public class ServerBitbucketCause extends BitbucketCause { 6 | 7 | private final String serverUrl; 8 | 9 | public ServerBitbucketCause(String serverUrl, String sourceBranch, String targetBranch, String repositoryOwner, 10 | String repositoryName, String repositoryUri, String pullRequestId, String destinationRepositoryOwner, 11 | String destinationRepositoryName, String pullRequestTitle, String sourceCommitHash, 12 | String destinationCommitHash, String pullRequestAuthor) { 13 | super(sourceBranch, targetBranch, repositoryOwner, repositoryName, repositoryUri, pullRequestId, 14 | destinationRepositoryOwner, destinationRepositoryName, pullRequestTitle, sourceCommitHash, 15 | destinationCommitHash, pullRequestAuthor); 16 | this.serverUrl = serverUrl; 17 | } 18 | 19 | @Override 20 | public String getShortDescription() { 21 | return "#" + getPullRequestId() + " " + getPullRequestTitle() 22 | + " (" + getServerUrl() + "/projects/" + getRepositoryOwner() + "/repos/" + getRepositoryName() + "/pull-requests/" + getPullRequestId() + ")"; 23 | } 24 | 25 | public String getServerUrl() { 26 | return serverUrl; 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/server/ServerPullrequest.java: -------------------------------------------------------------------------------- 1 | package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.server; 2 | 3 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.AbstractPullrequest; 4 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.BuildState; 5 | import org.apache.commons.lang.StringUtils; 6 | import org.codehaus.jackson.annotate.JsonIgnoreProperties; 7 | import org.codehaus.jackson.annotate.JsonProperty; 8 | 9 | import java.util.Map; 10 | import java.util.Objects; 11 | 12 | /** 13 | * POJOs representing the pull-requests extracted from the 14 | * JSON response of the Bitbucket Server API V1. 15 | * 16 | * https://docs.atlassian.com/bitbucket-server/rest/5.9.0/bitbucket-rest.html 17 | */ 18 | @JsonIgnoreProperties(ignoreUnknown = true) 19 | public class ServerPullrequest extends AbstractPullrequest { 20 | private String id; 21 | private String title; 22 | 23 | @JsonProperty("toRef") 24 | private Revision toRef; 25 | 26 | @JsonProperty("fromRef") 27 | private Revision fromRef; 28 | 29 | private String state; 30 | private Author author; 31 | 32 | @Override 33 | public String getTitle() { 34 | return title; 35 | } 36 | 37 | @Override 38 | public Revision getDestination() { 39 | return toRef; 40 | } 41 | 42 | @Override 43 | public Revision getSource() { 44 | return fromRef; 45 | } 46 | 47 | @Override 48 | public String getState() { 49 | return state; 50 | } 51 | 52 | @Override 53 | public String getId() { 54 | return id; 55 | } 56 | 57 | @Override 58 | public Author getAuthor() { 59 | return author; 60 | } 61 | 62 | @JsonIgnoreProperties(ignoreUnknown = true) 63 | public static class Revision implements AbstractPullrequest.Revision { 64 | private String id; 65 | private String latestCommit; 66 | private Repository repository; 67 | 68 | @Override 69 | public Repository getRepository() { 70 | return repository; 71 | } 72 | 73 | @Override 74 | public Branch getBranch() { 75 | return new Branch(id); 76 | } 77 | 78 | @Override 79 | public Commit getCommit() { 80 | return new Commit(latestCommit); 81 | } 82 | 83 | public void setId(String id) { 84 | this.id = StringUtils.remove(id, "refs/heads/"); 85 | } 86 | 87 | public void setLatestCommit(String latestCommit) { 88 | this.latestCommit = latestCommit; 89 | } 90 | 91 | public void setRepository(Repository repository) { 92 | this.repository = repository; 93 | } 94 | } 95 | 96 | @JsonIgnoreProperties(ignoreUnknown = true) 97 | public static class Repository implements AbstractPullrequest.Repository { 98 | private String name; 99 | private String slug; 100 | private String ownerName; 101 | private RepositoryLinks links; 102 | 103 | @JsonProperty("project") 104 | private void unpackProject(Map project) { 105 | this.ownerName = project.get("key").toString(); 106 | } 107 | 108 | public void setName(String name) { 109 | this.name = name; 110 | } 111 | 112 | public void setSlug(String slug) { 113 | this.slug = slug; 114 | } 115 | 116 | public void setLinks(RepositoryLinks links) { 117 | this.links = links; 118 | } 119 | 120 | @Override 121 | public String getName() { 122 | return name; 123 | } 124 | 125 | @Override 126 | public String getOwnerName() { 127 | return ownerName; 128 | } 129 | 130 | @Override 131 | public String getRepositoryName() { 132 | return slug; 133 | } 134 | 135 | @Override 136 | public RepositoryLinks getLinks() { 137 | return links; 138 | } 139 | } 140 | 141 | public static class Branch implements AbstractPullrequest.Branch { 142 | private String name; 143 | 144 | public Branch(final String name) { 145 | this.name = name; 146 | } 147 | 148 | @Override 149 | public String getName() { 150 | return name; 151 | } 152 | } 153 | 154 | public static class Commit implements AbstractPullrequest.Commit { 155 | private String hash; 156 | 157 | public Commit(final String hash) { 158 | this.hash = hash; 159 | } 160 | 161 | @Override 162 | public String getHash() { 163 | return hash; 164 | } 165 | } 166 | 167 | @JsonIgnoreProperties(ignoreUnknown = true) 168 | public static class Author implements AbstractPullrequest.Author{ 169 | 170 | private String username; 171 | private String displayName; 172 | 173 | @JsonProperty("user") 174 | private void unpackUser(Map user) { 175 | this.username = user.get("name").toString(); 176 | this.displayName = user.get("displayName").toString(); 177 | } 178 | 179 | @Override 180 | public String getUsername() { 181 | return username; 182 | } 183 | 184 | @Override 185 | public String getDisplayName() { 186 | return displayName; 187 | } 188 | 189 | @Override 190 | public String getCombinedUsername() { 191 | return String.format(AUTHOR_COMBINED_NAME, this.getDisplayName(), this.getUsername()); 192 | } 193 | } 194 | 195 | @JsonIgnoreProperties(ignoreUnknown = true) 196 | public static class Comment implements AbstractPullrequest.Comment { 197 | 198 | private Integer id; 199 | private String content; 200 | 201 | @Override 202 | public int compareTo(AbstractPullrequest.Comment target) { 203 | if (target == null){ 204 | return -1; 205 | } else if (this.getId() > target.getId()) { 206 | return 1; 207 | } else if (this.getId().equals(target.getId())) { 208 | return 0; 209 | } else { 210 | return -1; 211 | } 212 | } 213 | 214 | @Override 215 | public boolean equals(final Object o) { 216 | if (this == o) return true; 217 | if (o == null || getClass() != o.getClass()) return false; 218 | 219 | final Comment comment = (Comment) o; 220 | 221 | return getId() != null ? getId().equals(comment.getId()) : comment.getId() == null; 222 | } 223 | 224 | @Override 225 | public int hashCode() { 226 | return getId() != null ? getId().hashCode() : 0; 227 | } 228 | 229 | public void setId(Integer id) { 230 | this.id = id; 231 | } 232 | 233 | public void setContent(String content) { 234 | this.content = content; 235 | } 236 | 237 | @Override 238 | public Integer getId() { 239 | return id; 240 | } 241 | 242 | @Override 243 | @JsonProperty("text") 244 | public String getContent() { 245 | return content; 246 | } 247 | 248 | } 249 | 250 | @JsonIgnoreProperties(ignoreUnknown = true) 251 | public static class Activity { 252 | private String action; 253 | private Integer id; 254 | private String text; 255 | private String path; 256 | 257 | @JsonProperty("comment") 258 | private void unpackComment(Map comment) { 259 | this.id = Integer.valueOf(comment.get("id").toString()); 260 | this.text = comment.get("text").toString(); 261 | } 262 | 263 | @JsonProperty("commentAnchor") 264 | private void unpackAnchor(Map anchor) { 265 | this.path = anchor.get("path").toString(); 266 | } 267 | 268 | public boolean isComment() { 269 | return "COMMENTED".equals(this.action); 270 | } 271 | 272 | public Comment toComment() { 273 | final Comment comment = new Comment(); 274 | comment.setId(id); 275 | comment.setContent(text); 276 | return comment; 277 | } 278 | 279 | public String getAction() { 280 | return action; 281 | } 282 | 283 | @JsonProperty("action") 284 | public void setAction(String action) { 285 | this.action = action; 286 | } 287 | } 288 | 289 | @JsonIgnoreProperties(ignoreUnknown = true) 290 | public static class CommitBuildState { 291 | private String name; 292 | private String description; 293 | private BuildState state; 294 | private String key; 295 | private String url; 296 | 297 | public String getName() { 298 | return name; 299 | } 300 | 301 | public void setName(String name) { 302 | this.name = name; 303 | } 304 | 305 | public String getDescription() { 306 | return description; 307 | } 308 | 309 | public void setDescription(String description) { 310 | this.description = description; 311 | } 312 | 313 | public BuildState getState() { 314 | return state; 315 | } 316 | 317 | public void setState(BuildState state) { 318 | this.state = state; 319 | } 320 | 321 | public String getKey() { 322 | return key; 323 | } 324 | 325 | public void setKey(String key) { 326 | this.key = key; 327 | } 328 | 329 | public String getUrl() { 330 | return url; 331 | } 332 | 333 | public void setUrl(String url) { 334 | this.url = url; 335 | } 336 | } 337 | 338 | @JsonIgnoreProperties(ignoreUnknown = true) 339 | public static class Approver { 340 | private boolean approved; 341 | private String status; 342 | private User user; 343 | 344 | public boolean isApproved() { 345 | return approved; 346 | } 347 | 348 | public void setApproved(boolean approved) { 349 | this.approved = approved; 350 | } 351 | 352 | public String getStatus() { 353 | return status; 354 | } 355 | 356 | public void setStatus(String status) { 357 | this.status = status; 358 | } 359 | 360 | public User getUser() { 361 | return user; 362 | } 363 | 364 | public void setUser(User user) { 365 | this.user = user; 366 | } 367 | } 368 | 369 | @JsonIgnoreProperties(ignoreUnknown = true) 370 | public static class User { 371 | private String name; 372 | 373 | public User(String name) { 374 | this.name = name; 375 | } 376 | 377 | public String getName() { 378 | return name; 379 | } 380 | 381 | public void setName(String name) { 382 | this.name = name; 383 | } 384 | } 385 | 386 | } -------------------------------------------------------------------------------- /src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-bitbucketServer.html: -------------------------------------------------------------------------------- 1 | URL of the Bitbucket Server. Leave it blank if Bitbucket Cloud is used 2 | -------------------------------------------------------------------------------- /src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-branchesFilter.html: -------------------------------------------------------------------------------- 1 |
2 | Filter option in custom format. Default value is empty or any.
3 | Available formats: 4 |
    5 |
  • any pull requests applied for this project: any, * or empty string
  • 6 |
  • filtered by destination branch: my-branch or more complex reg-ex filter r:^master (must be started with r: and case insensitive match).
  • 7 |
  • filtered by source and destination branches: s:source-branch d:dest-branch
  • 8 |
  • filtered by source and destination branches with regex: s:r:^feature d:r:master$
  • 9 |
  • filtered by many destination/source branches: s:one s:two s:three d:master d:r:master$
  • 10 |
  • filtered by many sources branches: s:one s:two s:r:^three d:
  • 11 |
12 |

13 | 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:.
14 | Any sources and any destinations for pull request: 15 |

    16 |
  • filter string: *
  • 17 |
  • filter string: s: d:
  • 18 |
19 |
20 | -------------------------------------------------------------------------------- /src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-branchesFilterBySCMIncludes.html: -------------------------------------------------------------------------------- 1 | Uses the Git SCM option "Branches to build" option as the value for 2 | "BranchesFilter". If the "BranchesFilter" field itself has any content, 3 | it will be ignored. 4 |
5 | If the "Branches to build" option has values 6 | "*/master */feature-master */build-with-jenkins", then "BranchesFilter" 7 | field will have value "d:master d:feature-master d:build-with-jenkins". 8 | -------------------------------------------------------------------------------- /src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-buildChronologically.html: -------------------------------------------------------------------------------- 1 | Build Pull Requests in reverse order - older request will be build first -------------------------------------------------------------------------------- /src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-cancelOutdatedJobs.html: -------------------------------------------------------------------------------- 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. -------------------------------------------------------------------------------- /src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-ciKey.html: -------------------------------------------------------------------------------- 1 | The identifier needs to be unique among your Jenkins jobs related to this repo. 2 | This identifier is used to decide whether a commit is already built by this job and to set status for a newly built commit. 3 | If the value is changed rebuilds may occur and multiple statuses might show on an existing pull request. 4 | The value is not shown for end users of Bitbucket. 5 | -------------------------------------------------------------------------------- /src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-ciName.html: -------------------------------------------------------------------------------- 1 | This value is the name of the current job when showing build statuses for a pull request. 2 | -------------------------------------------------------------------------------- /src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-ciSkipPhrases.html: -------------------------------------------------------------------------------- 1 | A comma-separated list of strings to search the pull request title for. 2 |
3 | e.g. If set to "trivial,[skiptest]", any PRs containing either "trivial" or 4 | "[skiptest]" (case-insensitive) will not be built. 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help.html: -------------------------------------------------------------------------------- 1 |
2 |

Builds pull requests from Bitbucket.org and will report the test results.

3 |

This plugin requires Git SCM plugin configured as follows: 4 |

    5 |
  • Add Repository URL, git@bitbucket.org:${repositoryOwner}/${repositoryName}.git
  • 6 |
  • In Branch Specifier, type */${sourceBranch} 7 |
8 | 9 |
10 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 5 |
6 | This plugin polls BitBucket to determine whether there are Pull Requests that should be built. 7 |
8 | -------------------------------------------------------------------------------- /src/test/java/BitbucketBuildFilterTest.java: -------------------------------------------------------------------------------- 1 | 2 | import antlr.ANTLRException; 3 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketBuildFilter; 4 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.cloud.CloudBitbucketCause; 5 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketPullRequestsBuilder; 6 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketRepository; 7 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.AbstractPullrequest; 8 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.ApiClient; 9 | 10 | import java.util.Calendar; 11 | import java.util.LinkedList; 12 | import java.util.List; 13 | import java.util.regex.Pattern; 14 | 15 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.cloud.CloudPullrequest; 16 | import jenkins.plugins.git.AbstractGitSCMSource; 17 | import org.jvnet.hudson.test.JenkinsRule; 18 | import org.jvnet.hudson.test.WithoutJenkins; 19 | 20 | import org.easymock.*; 21 | import org.junit.Test; 22 | import org.junit.Rule; 23 | import static org.junit.Assert.*; 24 | 25 | /** 26 | * Tests 27 | */ 28 | public class BitbucketBuildFilterTest { 29 | 30 | @Rule 31 | public JenkinsRule jRule = new JenkinsRule(); 32 | 33 | @Test 34 | @WithoutJenkins 35 | public void mockTest() { 36 | CloudBitbucketCause cause = EasyMock.createMock(CloudBitbucketCause.class); 37 | EasyMock.expect(cause.getTargetBranch()).andReturn("mock").anyTimes(); 38 | EasyMock.replay(cause); 39 | for(Integer i : new Integer[] {1, 2, 3, 4, 5}) assertEquals("mock", cause.getTargetBranch()); 40 | } 41 | 42 | @Test 43 | @WithoutJenkins 44 | public void anyFilter() { 45 | CloudBitbucketCause cause = EasyMock.createMock(CloudBitbucketCause.class); 46 | EasyMock.expect(cause.getTargetBranch()).andReturn("master").anyTimes(); 47 | EasyMock.replay(cause); 48 | 49 | for(String f : new String[] {"", "*", "any"}) { 50 | BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); 51 | assertTrue(filter.approved(cause)); 52 | } 53 | 54 | for(String f : new String[] {"foo", "bar", " baz "}) { 55 | BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); 56 | assertFalse(filter.approved(cause)); 57 | } 58 | } 59 | 60 | @Test 61 | @WithoutJenkins 62 | public void onlyDestinationFilter() { 63 | CloudBitbucketCause cause = EasyMock.createMock(CloudBitbucketCause.class); 64 | EasyMock.expect(cause.getTargetBranch()).andReturn("master-branch").anyTimes(); 65 | EasyMock.replay(cause); 66 | 67 | for(String f : new String[] {"master-branch", "r:^master", "r:branch$", " master-branch "}) { 68 | BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); 69 | assertTrue(filter.approved(cause)); 70 | } 71 | 72 | for(String f : new String[] {"develop", "feature-good-thing", "r:develop$"}) { 73 | BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); 74 | assertFalse(filter.approved(cause)); 75 | } 76 | } 77 | 78 | @Test 79 | @WithoutJenkins 80 | public void rxSourceDestCheck() { 81 | for(String f : new String[] {"", "master", "r:master", "*"}) 82 | assertFalse(Pattern.compile("(s:)|(d:)").matcher(f).find()); 83 | 84 | for(String f : new String[] {"s:master d:feature-master", "s:master d:r:^feature", "s:r:^master d:r:^feature"}) 85 | assertTrue(Pattern.compile("(s:)|(d:)").matcher(f).find()); 86 | } 87 | 88 | @Test 89 | @WithoutJenkins 90 | public void sourceAndDestFilter() { 91 | CloudBitbucketCause cause = EasyMock.createMock(CloudBitbucketCause.class); 92 | EasyMock.expect(cause.getTargetBranch()).andReturn("master").anyTimes(); 93 | EasyMock.expect(cause.getSourceBranch()).andReturn("feature-for-master").anyTimes(); 94 | EasyMock.replay(cause); 95 | 96 | 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"}) { 97 | BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); 98 | assertTrue(filter.approved(cause)); 99 | } 100 | 101 | for(String f : new String[] {"s:feature-for-master d:foo", "s:bar d:master", "s:foo d:bar"}) { 102 | BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); 103 | assertFalse(filter.approved(cause)); 104 | } 105 | } 106 | 107 | @Test 108 | @WithoutJenkins 109 | public void multipleSrcDestFilter() { 110 | CloudBitbucketCause cause = EasyMock.createMock(CloudBitbucketCause.class); 111 | EasyMock.expect(cause.getTargetBranch()).andReturn("master").anyTimes(); 112 | EasyMock.expect(cause.getSourceBranch()).andReturn("feature-master").anyTimes(); 113 | EasyMock.replay(cause); 114 | 115 | 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:"}) { 116 | BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); 117 | assertTrue(filter.approved(cause)); 118 | } 119 | 120 | for(String f : new String[] {"d:ggg d:ooo d:333 s:feature-master", "s:111 s:222 s:333 d:master"}) { 121 | BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); 122 | assertFalse(filter.approved(cause)); 123 | } 124 | } 125 | 126 | @Test 127 | @WithoutJenkins 128 | public void sourceAndDestPartiallyFilter() { 129 | CloudBitbucketCause cause = EasyMock.createMock(CloudBitbucketCause.class); 130 | EasyMock.expect(cause.getTargetBranch()).andReturn("master").anyTimes(); 131 | EasyMock.expect(cause.getSourceBranch()).andReturn("feature-master").anyTimes(); 132 | EasyMock.replay(cause); 133 | 134 | for(String f : new String[] {"s:feature-master d:", "d:master s:"}) { 135 | BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); 136 | assertTrue(filter.approved(cause)); 137 | } 138 | 139 | for(String f : new String[] {"s:feature-master", "d:master"}) { 140 | BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); 141 | assertFalse(filter.approved(cause)); 142 | } 143 | } 144 | 145 | @Test 146 | @WithoutJenkins 147 | public void authorFilter() { 148 | CloudBitbucketCause cause = EasyMock.createMock(CloudBitbucketCause.class); 149 | EasyMock.expect(cause.getTargetBranch()).andReturn("master").anyTimes(); 150 | EasyMock.expect(cause.getSourceBranch()).andReturn("feature-master").anyTimes(); 151 | EasyMock.expect(cause.getPullRequestAuthor()).andReturn("test").anyTimes(); 152 | EasyMock.replay(cause); 153 | 154 | for(String f : new String[] {"a:test", "a:r:^test", "d: s: a:", "a:", "a:foo a:test"}) { 155 | BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); 156 | assertTrue(filter.approved(cause)); 157 | } 158 | 159 | for(String f : new String[] {"s:feature-master", "d:master", "s:feature-master d: a:foo", "a:bar"}) { 160 | BitbucketBuildFilter filter = BitbucketBuildFilter.instanceByString(f); 161 | assertFalse(filter.approved(cause)); 162 | } 163 | } 164 | 165 | @Test 166 | @WithoutJenkins 167 | public void emptyGitSCMFilter() { 168 | CloudBitbucketCause cause = EasyMock.createMock(CloudBitbucketCause.class); 169 | EasyMock.expect(cause.getTargetBranch()).andReturn("master").anyTimes(); 170 | EasyMock.replay(cause); 171 | 172 | assertTrue(BitbucketBuildFilter.filterFromGitSCMSource(null, "").isEmpty()); 173 | assertEquals("default", BitbucketBuildFilter.filterFromGitSCMSource(null, "default")); 174 | 175 | assertTrue(BitbucketBuildFilter.instanceByString( 176 | BitbucketBuildFilter.filterFromGitSCMSource(null, "")).approved(cause) 177 | ); 178 | } 179 | 180 | @Test 181 | @WithoutJenkins 182 | public void fromGitSCMFilter() { 183 | AbstractGitSCMSource git = EasyMock.createMock(AbstractGitSCMSource.class); 184 | EasyMock.expect(git.getIncludes()) 185 | .andReturn("").times(1) 186 | .andReturn("").times(1) 187 | .andReturn("*/master */feature-branch").times(1) 188 | .andReturn("*/master").anyTimes(); 189 | EasyMock.replay(git); 190 | 191 | assertTrue(git.getIncludes().isEmpty()); 192 | assertEquals("", BitbucketBuildFilter.filterFromGitSCMSource(git, "")); 193 | assertEquals("d:master d:feature-branch", BitbucketBuildFilter.filterFromGitSCMSource(git, "")); 194 | assertEquals("d:master", BitbucketBuildFilter.filterFromGitSCMSource(git, "")); 195 | } 196 | 197 | @Test 198 | @WithoutJenkins 199 | public void filterPRComments() throws ANTLRException { 200 | BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); 201 | EasyMock.expect(builder.getTrigger()).andReturn(null).anyTimes(); 202 | EasyMock.replay(builder); 203 | 204 | List comments = new LinkedList<>(); 205 | for(String commentContent : new String[] { 206 | "check", 207 | "", 208 | "Hello from mock", 209 | "Jenkins: test this please", 210 | "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970]", 211 | "check", 212 | "", 213 | "Hello from mock", 214 | "Jenkins: test this please", 215 | "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970]", 216 | "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970 #jenkins-foo]", 217 | "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970 #jenkins-foo #jenkins-bar]", 218 | }) { 219 | CloudPullrequest.Comment comment = EasyMock.createNiceMock(CloudPullrequest.Comment.class); 220 | EasyMock.expect(comment.getContent()).andReturn(commentContent).anyTimes(); 221 | EasyMock.expect(comment.getId()).andReturn(new java.sql.Timestamp(Calendar.getInstance().getTime().getTime()).getNanos()).anyTimes(); 222 | EasyMock.replay(comment); 223 | comments.add(comment); 224 | } 225 | 226 | // Check twice 227 | assertEquals("check", comments.get(0).getContent()); 228 | assertEquals("check", comments.get(0).getContent()); 229 | 230 | assertEquals("Hello from mock", comments.get(2).getContent()); 231 | 232 | BitbucketRepository repo = new BitbucketRepository("", builder); 233 | repo.init(EasyMock.createNiceMock(ApiClient.class)); 234 | 235 | List filteredComments = repo.filterPullRequestComments(comments); 236 | 237 | assertTrue(filteredComments.size() == 4); 238 | assertEquals("Jenkins: test this please", filteredComments.get(filteredComments.size() - 1).getContent()); 239 | } 240 | 241 | @Test 242 | @WithoutJenkins 243 | public void checkHashMyBuildTagTrue() { 244 | BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); 245 | EasyMock.expect(builder.getTrigger()).andReturn(null).anyTimes(); 246 | EasyMock.replay(builder); 247 | 248 | IMockBuilder repoBuilder = EasyMock.partialMockBuilder(BitbucketRepository.class); 249 | repoBuilder.addMockedMethod("getMyBuildTag"); 250 | BitbucketRepository repo = repoBuilder.createMock(); 251 | EasyMock.expect(repo.getMyBuildTag(EasyMock.anyString())).andReturn("#jenkins-902f259e962ff16100843123480a0970").anyTimes(); 252 | EasyMock.replay(repo); 253 | 254 | List comments = new LinkedList(); 255 | for(String commentContent : new String[] { 256 | "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970]", 257 | "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970 #jenkins-foo]", 258 | "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970 #jenkins-foo #jenkins-bar]", 259 | "TTP build flag ```[bid: #jenkins-902f259e962ff16100843123480a0970 #jenkins-foo #jenkins-bar]```", 260 | }) { 261 | CloudPullrequest.Comment comment = EasyMock.createNiceMock(CloudPullrequest.Comment.class); 262 | EasyMock.expect(comment.getContent()).andReturn(commentContent).anyTimes(); 263 | EasyMock.expect(comment.getId()).andReturn(new java.sql.Timestamp(Calendar.getInstance().getTime().getTime()).getNanos()).anyTimes(); 264 | EasyMock.replay(comment); 265 | comments.add(comment); 266 | } 267 | 268 | String myBuildKey = "902f259e962ff16100843123480a0970"; 269 | for(CloudPullrequest.Comment comment : comments) 270 | assertTrue(repo.hasMyBuildTagInTTPComment(comment.getContent(), myBuildKey)); 271 | } 272 | 273 | @Test 274 | @WithoutJenkins 275 | public void checkHashMyBuildTagFalse() { 276 | BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); 277 | EasyMock.expect(builder.getTrigger()).andReturn(null).anyTimes(); 278 | EasyMock.replay(builder); 279 | 280 | IMockBuilder repoBuilder = EasyMock.partialMockBuilder(BitbucketRepository.class); 281 | repoBuilder.addMockedMethod("getMyBuildTag"); 282 | BitbucketRepository repo = repoBuilder.createMock(); 283 | EasyMock.expect(repo.getMyBuildTag(EasyMock.anyString())).andReturn("#jenkins-902f259e962ff16100843123480a0970").anyTimes(); 284 | EasyMock.replay(repo); 285 | 286 | List comments = new LinkedList(); 287 | for(String commentContent : new String[] { 288 | "check", 289 | "", 290 | "Hello from mock", 291 | "Jenkins: test this please", 292 | "TTP build flag [bid: #jenkins]", 293 | "TTP build flag [bid: #jenkins-foo]", 294 | "TTP build flag [bid: #jenkins-foo #jenkins-bar]", 295 | "TTP build flag ```[bid: #jenkins-foo #jenkins-bar]```", 296 | }) { 297 | CloudPullrequest.Comment comment = EasyMock.createNiceMock(CloudPullrequest.Comment.class); 298 | EasyMock.expect(comment.getContent()).andReturn(commentContent).anyTimes(); 299 | EasyMock.expect(comment.getId()).andReturn(new java.sql.Timestamp(Calendar.getInstance().getTime().getTime()).getNanos()).anyTimes(); 300 | EasyMock.replay(comment); 301 | comments.add(comment); 302 | } 303 | 304 | String myBuildKey = "902f259e962ff16100843123480a0970"; 305 | for(CloudPullrequest.Comment comment : comments) 306 | assertFalse(repo.hasMyBuildTagInTTPComment(comment.getContent(), myBuildKey)); 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/test/java/BitbucketBuildRepositoryTest.java: -------------------------------------------------------------------------------- 1 | 2 | import antlr.ANTLRException; 3 | 4 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketBuildTrigger; 5 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketPullRequestsBuilder; 6 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketRepository; 7 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.AbstractPullrequest; 8 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.ApiClient; 9 | 10 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.cloud.CloudApiClient; 11 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.cloud.CloudPullrequest; 12 | import com.cloudbees.plugins.credentials.CredentialsProvider; 13 | import com.cloudbees.plugins.credentials.CredentialsScope; 14 | import com.cloudbees.plugins.credentials.CredentialsStore; 15 | import com.cloudbees.plugins.credentials.domains.Domain; 16 | import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; 17 | 18 | import com.google.common.base.Function; 19 | import com.google.common.collect.Collections2; 20 | 21 | import java.io.UnsupportedEncodingException; 22 | import java.security.MessageDigest; 23 | import java.security.NoSuchAlgorithmException; 24 | import java.util.ArrayList; 25 | import java.util.Arrays; 26 | import java.util.Collection; 27 | import java.util.List; 28 | import java.util.logging.Logger; 29 | import org.easymock.*; 30 | import org.junit.Test; 31 | import static org.junit.Assert.*; 32 | import org.junit.Rule; 33 | import org.junit.Assert; 34 | import org.jvnet.hudson.test.JenkinsRule; 35 | 36 | import jenkins.model.Jenkins; 37 | 38 | import org.apache.commons.codec.binary.Hex; 39 | import org.apache.commons.httpclient.Credentials; 40 | import org.apache.commons.httpclient.HttpClient; 41 | import org.apache.commons.httpclient.HttpState; 42 | import org.apache.commons.httpclient.UsernamePasswordCredentials; 43 | import org.apache.commons.httpclient.auth.AuthScope; 44 | 45 | 46 | interface ICredentialsInterceptor { 47 | void assertCredentials(Credentials actual); 48 | } 49 | 50 | /** 51 | * Utility class for interceptor functionality 52 | * @param 53 | */ 54 | class HttpClientInterceptor extends HttpClient { 55 | private static final Logger logger = Logger.getLogger(HttpClientInterceptor.class.getName()); 56 | 57 | class CredentialsInterceptor extends HttpState { 58 | private final T interceptor; 59 | public CredentialsInterceptor(T interceptor) { this.interceptor = interceptor; } 60 | 61 | @Override 62 | public synchronized void setCredentials(AuthScope authscope, Credentials credentials) { 63 | logger.fine("Inject setCredentials"); 64 | super.setCredentials(authscope, credentials); 65 | this.interceptor.assertCredentials(credentials); 66 | throw new AssertionError(); 67 | } 68 | } 69 | 70 | private final T interceptor; 71 | public HttpClientInterceptor(T interceptor) { this.interceptor = interceptor; } 72 | 73 | @Override 74 | public synchronized HttpState getState() { return new CredentialsInterceptor(this.interceptor); } 75 | } 76 | 77 | /** 78 | * Utility class for credentials assertion 79 | * Used with 80 | * @author maxvodo 81 | */ 82 | class AssertCredentials implements ICredentialsInterceptor { 83 | private static final Logger logger = Logger.getLogger(AssertCredentials.class.getName()); 84 | 85 | private final Credentials expected; 86 | public AssertCredentials(Credentials expected) { this.expected = expected; } 87 | 88 | public void assertCredentials(Credentials actual) { 89 | logger.fine("Assert credential"); 90 | if (actual == null) assertTrue(this.expected == null); 91 | else assertTrue(this.expected != null); 92 | 93 | if (actual instanceof UsernamePasswordCredentials) { 94 | UsernamePasswordCredentials actual_ = (UsernamePasswordCredentials)actual, 95 | expected_ = (UsernamePasswordCredentials)this.expected; 96 | assertNotNull(expected_); 97 | Assert.assertArrayEquals(new Object[] { 98 | actual_.getUserName(), actual_.getPassword() 99 | }, new Object[] { 100 | expected_.getUserName(), expected_.getPassword() 101 | }); 102 | } 103 | } 104 | } 105 | 106 | /** 107 | * Tests 108 | */ 109 | public class BitbucketBuildRepositoryTest { 110 | 111 | @Rule 112 | public JenkinsRule jRule = new JenkinsRule(); 113 | 114 | @Test 115 | public void repositorySimpleUserPasswordTest() throws Exception { 116 | BitbucketBuildTrigger trigger = new BitbucketBuildTrigger( 117 | "", "","@hourly", 118 | "JenkinsCID", 119 | "foo", 120 | "bar", 121 | "", "", 122 | "", true, 123 | "", "", "", 124 | true, 125 | true, 126 | true, 127 | false, BitbucketRepository.DEFAULT_COMMENT_TRIGGER 128 | ); 129 | 130 | BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); 131 | EasyMock.expect(builder.getTrigger()).andReturn(trigger).anyTimes(); 132 | EasyMock.replay(builder); 133 | 134 | ApiClient.HttpClientFactory httpFactory = EasyMock.createNiceMock(ApiClient.HttpClientFactory.class); 135 | EasyMock.expect(httpFactory.getInstanceHttpClient()).andReturn( 136 | new HttpClientInterceptor(new AssertCredentials(new UsernamePasswordCredentials("foo", "bar"))) 137 | ).anyTimes(); 138 | EasyMock.replay(httpFactory); 139 | 140 | BitbucketRepository repo = new BitbucketRepository("", builder); 141 | repo.init(httpFactory); 142 | 143 | try { repo.postPullRequestApproval("prId"); } catch(Error e) { assertTrue(e instanceof AssertionError); } 144 | } 145 | 146 | @Test 147 | public void repositoryCtorWithTriggerTest() throws Exception { 148 | BitbucketBuildTrigger trigger = new BitbucketBuildTrigger( 149 | "", "","@hourly", 150 | "JenkinsCID", 151 | "foo", 152 | "bar", 153 | "", "", 154 | "", true, 155 | "", "", "", 156 | true, 157 | true, 158 | true, 159 | false, BitbucketRepository.DEFAULT_COMMENT_TRIGGER 160 | ); 161 | 162 | BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); 163 | EasyMock.expect(builder.getTrigger()).andReturn(trigger).anyTimes(); 164 | EasyMock.replay(builder); 165 | 166 | CredentialsStore store = CredentialsProvider.lookupStores(Jenkins.getInstance()).iterator().next(); 167 | assertNotNull(store); 168 | store.addCredentials(Domain.global(), new UsernamePasswordCredentialsImpl( 169 | CredentialsScope.GLOBAL, "JenkinsCID", "description", "username", "password" 170 | )); 171 | 172 | ApiClient.HttpClientFactory httpFactory = EasyMock.createNiceMock(ApiClient.HttpClientFactory.class); 173 | EasyMock.expect(httpFactory.getInstanceHttpClient()).andReturn( 174 | new HttpClientInterceptor(new AssertCredentials(new UsernamePasswordCredentials("username", "password"))) 175 | ).anyTimes(); 176 | EasyMock.replay(httpFactory); 177 | 178 | BitbucketRepository repo = new BitbucketRepository("", builder); 179 | repo.init(httpFactory); 180 | 181 | try { repo.postPullRequestApproval("prId"); } catch(Error e) { assertTrue(e instanceof AssertionError); } 182 | } 183 | 184 | class MD5HasherFunction implements Function { 185 | protected final MessageDigest MD5; 186 | public MD5HasherFunction(MessageDigest md5) { this.MD5 = md5; } 187 | public String apply(String f) { 188 | try { return new String(Hex.encodeHex(MD5.digest(f.getBytes("UTF-8")))); } catch(UnsupportedEncodingException e) { } 189 | return null; 190 | } 191 | } 192 | 193 | class SHA1HasherFunction implements Function { 194 | protected final MessageDigest SHA1; 195 | public SHA1HasherFunction(MessageDigest sha1) { this.SHA1 = sha1; } 196 | public String apply(String f) { 197 | try { return new String(Hex.encodeHex(SHA1.digest(f.getBytes("UTF-8")))); } catch(UnsupportedEncodingException e) { } 198 | return null; 199 | } 200 | } 201 | 202 | @Test 203 | public void repositoryProjectIdTest() throws ANTLRException, NoSuchAlgorithmException, UnsupportedEncodingException { 204 | BitbucketBuildTrigger trigger = new BitbucketBuildTrigger( 205 | "", "","@hourly", 206 | "JenkinsCID", 207 | "foo", 208 | "bar", 209 | "", "", 210 | "", true, 211 | "jenkins", "Jenkins", "", 212 | true, 213 | true, 214 | true, 215 | false, BitbucketRepository.DEFAULT_COMMENT_TRIGGER 216 | ); 217 | 218 | BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); 219 | EasyMock.expect(builder.getTrigger()).andReturn(trigger).anyTimes(); 220 | 221 | final MessageDigest MD5 = MessageDigest.getInstance("MD5"); 222 | 223 | String[] projectIds = new String[] { 224 | "one", 225 | "Second project", 226 | "Project abstract 1.1", 227 | "Good project, careated at " + (new java.util.Date()).toString(), 228 | }; 229 | 230 | Collection hashedProjectIdsCollection = Collections2.transform(Arrays.asList(projectIds), new MD5HasherFunction(MD5)); 231 | String[] hashedPojectIds = hashedProjectIdsCollection.toArray(new String[hashedProjectIdsCollection.size()]); 232 | 233 | for(String projectId : hashedPojectIds) { 234 | EasyMock.expect(builder.getProjectId()).andReturn(projectId).times(1); 235 | } 236 | EasyMock.replay(builder); 237 | 238 | BitbucketRepository repo = new BitbucketRepository("", builder); 239 | repo.init(); 240 | 241 | for(String projectId : projectIds) { 242 | String hashMD5 = new String(Hex.encodeHex(MD5.digest(projectId.getBytes("UTF-8")))); 243 | String buildStatusKey = repo.getClient().buildStatusKey(builder.getProjectId()); 244 | 245 | assertTrue(buildStatusKey.length() <= ApiClient.MAX_KEY_SIZE_BB_API); 246 | assertEquals(buildStatusKey, "jenkins-" + hashMD5); 247 | } 248 | } 249 | 250 | @Test 251 | public void triggerLongCIKeyTest() throws ANTLRException, NoSuchAlgorithmException { 252 | BitbucketBuildTrigger trigger = new BitbucketBuildTrigger( 253 | "", "","@hourly", 254 | "JenkinsCID", 255 | "foo", 256 | "bar", 257 | "", "", 258 | "", true, 259 | "jenkins-too-long-ci-key", "Jenkins", "", 260 | true, 261 | true, 262 | true, 263 | false, BitbucketRepository.DEFAULT_COMMENT_TRIGGER 264 | ); 265 | 266 | final MessageDigest MD5 = MessageDigest.getInstance("MD5"); 267 | final MessageDigest SHA1 = MessageDigest.getInstance("SHA1"); 268 | 269 | BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); 270 | EasyMock.expect(builder.getTrigger()).andReturn(trigger).anyTimes(); 271 | EasyMock.expect(builder.getProjectId()).andReturn((new MD5HasherFunction(MD5)).apply("projectId")).anyTimes(); 272 | EasyMock.replay(builder); 273 | 274 | BitbucketRepository repo = new BitbucketRepository("", builder); 275 | repo.init(); 276 | 277 | String buildStatusKey = repo.getClient().buildStatusKey(builder.getProjectId()); 278 | assertTrue(buildStatusKey.length() <= ApiClient.MAX_KEY_SIZE_BB_API); 279 | assertFalse(buildStatusKey.startsWith("jenkins-")); 280 | assertEquals((new SHA1HasherFunction(SHA1)).apply("jenkins-too-long-ci-key" + "-" + builder.getProjectId()), buildStatusKey); 281 | } 282 | 283 | @Test 284 | public void getTargetPullRequestsWithNullDestinationCommit() throws Exception { 285 | // arrange 286 | 287 | // setup mock BitbucketBuildTrigger 288 | final BitbucketBuildTrigger trigger = EasyMock.createMock(BitbucketBuildTrigger.class); 289 | EasyMock.expect(trigger.getCiSkipPhrases()).andReturn(""); 290 | EasyMock.expect(trigger.getBranchesFilterBySCMIncludes()).andReturn(false); 291 | EasyMock.expect(trigger.getBranchesFilter()).andReturn(""); 292 | EasyMock.expect(trigger.isCloud()).andReturn(true); 293 | EasyMock.expect(trigger.getBuildChronologically()).andReturn(true); 294 | EasyMock.expect(trigger.getBitbucketServer()).andReturn(null); 295 | EasyMock.replay(trigger); 296 | 297 | // setup mock BitbucketPullRequestsBuilder 298 | final BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); 299 | EasyMock.expect(builder.getTrigger()).andReturn(trigger).anyTimes(); 300 | EasyMock.expect(builder.getProjectId()).andReturn("").anyTimes(); 301 | EasyMock.replay(builder); 302 | 303 | // setup PRs to return from mock ApiClient 304 | final CloudPullrequest pullRequest = new CloudPullrequest(); 305 | 306 | final CloudPullrequest.Repository sourceRepo = new CloudPullrequest.Repository(); 307 | sourceRepo.setFullName("Owner/Name"); 308 | 309 | final CloudPullrequest.Repository destRepo = new CloudPullrequest.Repository(); 310 | destRepo.setFullName("Owner/Name"); 311 | 312 | final CloudPullrequest.Branch sourceBranch = new CloudPullrequest.Branch(); 313 | sourceBranch.setName("Name"); 314 | 315 | final CloudPullrequest.Branch destBranch = new CloudPullrequest.Branch(); 316 | destBranch.setName("Name"); 317 | 318 | final CloudPullrequest.Commit sourceCommit = new CloudPullrequest.Commit(); 319 | sourceCommit.setHash("Hash"); 320 | 321 | final CloudPullrequest.Commit destCommit = new CloudPullrequest.Commit(); 322 | destCommit.setHash(null); 323 | 324 | final CloudPullrequest.Revision sourceRevision = new CloudPullrequest.Revision(); 325 | sourceRevision.setBranch(sourceBranch); 326 | sourceRevision.setRepository(sourceRepo); 327 | sourceRevision.setCommit(sourceCommit); 328 | 329 | final CloudPullrequest.Revision destRevision = new CloudPullrequest.Revision(); 330 | destRevision.setBranch(destBranch); 331 | destRevision.setRepository(destRepo); 332 | destRevision.setCommit(destCommit); 333 | 334 | final CloudPullrequest.Author author = new CloudPullrequest.Author(); 335 | author.setDisplayName("DisplayName"); 336 | author.setUsername("Username"); 337 | 338 | pullRequest.setSource(sourceRevision); 339 | pullRequest.setDestination(destRevision); 340 | pullRequest.setId("Id"); 341 | pullRequest.setTitle("Title"); 342 | pullRequest.setState("OPEN"); 343 | pullRequest.setAuthor(author); 344 | 345 | final List pullRequests = new ArrayList<>(Arrays.asList(pullRequest)); 346 | 347 | // setup mock ApiClient 348 | final CloudApiClient client = EasyMock.createNiceMock(CloudApiClient.class); 349 | EasyMock.expect(client.getPullRequests()).andReturn(pullRequests); 350 | EasyMock.replay(client); 351 | 352 | // setup SUT 353 | final BitbucketRepository repo = new BitbucketRepository("", builder); 354 | 355 | // act 356 | repo.init(client); 357 | 358 | // assert 359 | Collection targetPullRequests = repo.getTargetPullRequests(); 360 | 361 | assertEquals(pullRequests.size(), targetPullRequests.size()); 362 | assertEquals(pullRequest, targetPullRequests.iterator().next()); 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /src/test/java/BitbucketCloudTest.java: -------------------------------------------------------------------------------- 1 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.ApiClient; 2 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.cloud.CloudApiClient; 3 | import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.cloud.CloudPullrequest; 4 | 5 | import java.util.logging.Logger; 6 | import java.util.List; 7 | import org.codehaus.jackson.map.ObjectMapper; 8 | import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; 9 | import org.codehaus.jackson.type.TypeReference; 10 | 11 | import org.jvnet.hudson.test.JenkinsRule; 12 | import org.jvnet.hudson.test.WithoutJenkins; 13 | 14 | import org.easymock.*; 15 | import org.junit.Test; 16 | import org.junit.Rule; 17 | import static org.junit.Assert.*; 18 | 19 | /** 20 | * Tests 21 | */ 22 | public class BitbucketCloudTest { 23 | 24 | private static Logger log = Logger.getLogger(BitbucketCloudTest.class.getName()); 25 | 26 | @Rule 27 | public JenkinsRule jRule = new JenkinsRule(); 28 | 29 | private R parse(String json, TypeReference ref) throws java.io.IOException { 30 | return new ObjectMapper().readValue(json, ref); 31 | } 32 | 33 | @Test 34 | @WithoutJenkins 35 | public void simpleTest() throws java.io.IOException { 36 | CloudPullrequest.Comment comment = new CloudPullrequest.Comment("This is a comment"); 37 | 38 | log.info("I'm starting"); 39 | String jsonStr = ApiClient.serializeObject(comment); 40 | log.info("Got thhis json: " + jsonStr + "\n"); 41 | assertEquals("{\"content\":{\"raw\":\"This is a comment\"}}", jsonStr); 42 | } 43 | 44 | @Test 45 | @WithoutJenkins 46 | // Simple test of the json parser, since the cloud api's abstraction is, urm, 47 | // contrived . . . 48 | public void cloudCommentsFromJsonTest() throws java.io.IOException { 49 | 50 | final CloudApiClient cloudApi = EasyMock.createNiceMock(CloudApiClient.class); 51 | 52 | CloudPullrequest.Comment comment = null; 53 | String commentStr = null; 54 | 55 | // Test 1 - extra fields 56 | commentStr = "{\"content\": {" + 57 | "\"html\": \"

rebuild please

\"," + 58 | "\"raw\": \"rebuild please\"" + 59 | "}," + 60 | "\"created_on\": \"2019-01-17T18:59:22.173394+00:00\"," + 61 | "\"id\": 88463806}"; 62 | 63 | comment = parse(commentStr, new TypeReference() {}); 64 | assertEquals("Comment Mismatch", "rebuild please", comment.getContent()); 65 | assertTrue("Id Mismatch", comment.getId() == 88463806); 66 | 67 | // Test 2 - a TPP 68 | commentStr = "{\"content\": {" + 69 | "\"raw\": \"TTP build flag ```[bid: #jenkins-7d061ab3f0531c7c7514cabf6cb81be5]```\"}," + 70 | "\"id\": 88464241}"; 71 | comment = parse(commentStr, new TypeReference() {}); 72 | assertTrue("Comment Mismatch", comment.getContent().contains("jenkins-7d06")); 73 | assertTrue("Id Mismatch", comment.getId() == 88464241); 74 | 75 | // Test 3 - Minimal with trigger phrase 76 | commentStr = "{\"content\": {\"raw\": \"rebuild please\"},\"id\": 88469113}"; 77 | comment = parse(commentStr, new TypeReference() {}); 78 | assertEquals("Comment Mismatch", "rebuild please", comment.getContent()); 79 | assertTrue("Id Mismatch", comment.getId() == 88469113); 80 | } 81 | } 82 | --------------------------------------------------------------------------------