├── .gitignore ├── COPYING ├── README.md ├── pom.xml └── src ├── main ├── java │ └── stashpullrequestbuilder │ │ └── stashpullrequestbuilder │ │ ├── StashAditionalParameterEnvironmentContributor.java │ │ ├── StashBuildListener.java │ │ ├── StashBuildTrigger.java │ │ ├── StashBuilds.java │ │ ├── StashCause.java │ │ ├── StashPostBuildComment.java │ │ ├── StashPostBuildCommentAction.java │ │ ├── StashPullRequestsBuilder.java │ │ ├── StashRepository.java │ │ └── stash │ │ ├── StashApiClient.java │ │ ├── StashPullRequestActivity.java │ │ ├── StashPullRequestActivityResponse.java │ │ ├── StashPullRequestComment.java │ │ ├── StashPullRequestMergableResponse.java │ │ ├── StashPullRequestMergableVetoMessage.java │ │ ├── StashPullRequestResponse.java │ │ ├── StashPullRequestResponseValue.java │ │ ├── StashPullRequestResponseValueRepository.java │ │ ├── StashPullRequestResponseValueRepositoryBranch.java │ │ ├── StashPullRequestResponseValueRepositoryCommit.java │ │ ├── StashPullRequestResponseValueRepositoryProject.java │ │ └── StashPullRequestResponseValueRepositoryRepository.java └── resources │ ├── index.jelly │ └── stashpullrequestbuilder │ └── stashpullrequestbuilder │ ├── StashBuildTrigger │ └── config.jelly │ └── StashPostBuildComment │ ├── config.jelly │ ├── help-buildFailedComment.html │ └── help-buildSuccessfulComment.html └── test └── java └── stashpullrequestbuilder └── stashpullrequestbuilder └── stash ├── AdditionalParameterRegExTest.java ├── StashApiClientTest.java └── StashPullRequestResponseValueRepositoryTest.java /.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 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright 2014 Nathan McCarthy. 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 Nathan McCarthy "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 Nathan McCarthy 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Stash Pull Request Builder Plugin 2 | ================================ 3 | 4 | > ## This plugin is now maintained in the offical Jenkins org; https://github.com/jenkinsci/stash-pullrequest-builder-plugin Please direct all PRs there. 5 | > ## Issues can be raised here; https://issues.jenkins.io/browse/JENKINS-63802?jql=project%20%3D%20JENKINS%20AND%20component%20%3D%20stash-pullrequest-builder-plugin 6 | 7 | [![Join the chat at https://gitter.im/nemccarthy/stash-pullrequest-builder-plugin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/nemccarthy/stash-pullrequest-builder-plugin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 8 | 9 | This Jenkins plugin builds pull requests from a Atlassian Stash server and will report the test results as a comment. 10 | This plugin was inspired by the GitHub & BitBucket pull request builder plugins. 11 | 12 | - Official [Jenkins Plugin Page](https://wiki.jenkins-ci.org/display/JENKINS/Stash+pullrequest+builder+plugin) 13 | - See this [blogpost](http://blog.nemccarthy.me/?p=387) for more details 14 | 15 | 16 | ## Prerequisites 17 | 18 | - Jenkins 1.532 or higher. 19 | - [Git Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Git+Plugin) 20 | 21 | ## Parameter variables 22 | 23 | The plugin makes available to the job the following parameter variables: 24 | - `${pullRequestId}` 25 | - `${pullRequestTitle}` 26 | - `${sourceBranch}` 27 | - `${targetBranch}` 28 | - `${sourceRepositoryOwner}` 29 | - `${sourceRepositoryName}` 30 | - `${destinationRepositoryOwner}` 31 | - `${destinationRepositoryName}` 32 | - `${sourceCommitHash}` 33 | - `${destinationCommitHash}` 34 | 35 | ## Creating a Job 36 | 37 | **Source Code Management** 38 | 39 | Select *Git* then configure: 40 | 41 | - **Repository URL**: `git@example.com:/${destinationRepositoryOwner}/${destinationRepositoryName}.git` 42 | - **Advance -> Refspec**: `+refs/pull-requests/*:refs/remotes/origin/pr/*` 43 | - **Branch Specifier**: `origin/pr/${pullRequestId}/from` 44 | 45 | **Build Triggers** 46 | 47 | Select *Stash Pull Request Builder* then configure: 48 | 49 | - **Cron**: must be specified. eg: every 2 minute `H/2 * * * *` 50 | - **Stash Host**: the *http* or *https* URL of the Stash host (NOT *ssh*). eg: *https://example.com* 51 | - **Stash Credentials**: Select or Add the login username/password for the Stash Host 52 | - **Project**: abbreviated project code. eg: *PRJ* or *~user* 53 | - **RepositoryName**: eg: *Repo* 54 | 55 | **Advanced options** 56 | - Ignore ssl certificates: 57 | - Build PR targetting only these branches: common separated list of branch names (or regexes). Blank for all. 58 | - Rebuild if destination branch changes: 59 | - Build only if Stash reports no conflicts: this should be set if using the merge branch to avoid issues with out of data merge branch in stash 60 | - Build only if PR is mergeable (note this will stop the PR being built if you have required approvers limit set >0 and the PR hasn't been approved) 61 | - Cancel outdated jobs 62 | - CI Skip Phrases: default: "NO TEST" 63 | - Only build when asked (with test phrase): 64 | - CI Build Phrases: default: "test this please" 65 | - Target branches: a comma separated list of branches (e.g. brancha,branchb) 66 | 67 | ## Building the merge of Source Branch into Target Branch 68 | 69 | You may want Jenkins to build the merged PR (that is the merge of `sourceBranch` into `targetBranch`) to catch any issues resulting from this. To do this change the Branch Specifier from `origin/pr/${pullRequestId}/from` to `origin/pr/${pullRequestId}/merge` 70 | 71 | If you are building the merged PR you probably want Jenkins to do a new build when the target branch changes. There is an advanced option in the build trigger, "Rebuild if destination branch changes?" which enables this. 72 | 73 | You probably also only want to build if the PR was mergeable and always without conflicts. There are advanced options in the build trigger for both of these. 74 | 75 | **NOTE: *Always enable `Build only if Stash reports no conflicts` if using the merge RefSpec!*** This will make sure the lazy merge on stash has happened before the build is triggered. 76 | 77 | #### Merging Locally 78 | If you dont want to use the lazy merged Stash PR RefSpec (described above) the other option is to do the merge locally as part of the build using the Jenkins git plugin (these only work for branches within the same repo); 79 | 80 | 1. Select Git SCM 81 | 2. Add Repository URL as bellow 82 | `git@myStashHost.com:${projectCode}/${repositoryName}.git` 83 | 3. In Branch Specifier, type as bellow 84 | `*/${sourceBranch}` 85 | 4. In the "Source Code Management" > "Git" > "Additional Behaviors" section, click "Add" > "Merge Before Building" 86 | 5. 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".) 87 | 6. In "Branch to merge to" put `${targetBranch}` 88 | - Note that as long as you don't push these changes to your remote repository, the merge only happens in your local repository. 89 | 90 | Alternatively if you want to use Stash's `origin/pr/${pullRequestId}/from` branch specifier and merge locally to avoid the race condition without checking stash if the PR is conflicted using the `merge` branch spec then; 91 | 92 | 1. Use the `origin/pr/${pullRequestId}/from` branch specifier 93 | 2. In the "Source Code Management" > "Git" > "Additional Behaviors" section, click "Add" > "Merge Before Building" 94 | 3. Set the "Branch to merge to" to `${targetBranch}` 95 | 4. Alternatively to the above 3 steps, just run a `git merge $destinationCommitHash` 96 | 97 | If you have downstream jobs that are not triggered by this plugin you can simply add a if condition on this command to check if the parameters are available; 98 | 99 | ``` 100 | if [ ! -z "$destinationCommitHash" ]; then 101 | git merge $destinationCommitHash 102 | fi 103 | ``` 104 | 105 | ## Notify Stash of build result 106 | If you are using the [StashNotifier plugin](https://wiki.jenkins-ci.org/display/JENKINS/StashNotifier+Plugin) and have enabled the 'Notify Stash Instance' Post-build Action while building the merged PR, you need to set `${sourceCommitHash}` as Commit SHA-1 to record the build result against the source commit. 107 | 108 | ## Rerun test builds 109 | 110 | If you want to rerun pull request test, write *"test this please"* comment to your pull request. 111 | 112 | ##Adding additional parameters to a build 113 | 114 | If you want to add additional parameters to the triggered build, add comments using the pattern `p:=`, one at each line, prefixed with `p:`. If the same parameter name appears multiple times the latest comment with that parameter will decide the value. 115 | 116 | **Example:** 117 | 118 | test this please 119 | p:country=USA 120 | p:env=dev1 121 | 122 | 123 | ## Post Build Comment 124 | 125 | It is possible to add a post build action that gives the option to post additional information to Stash when a build has been either successful or failed. 126 | These comments can contain environment variables that will be translated when posted to Stash. 127 | 128 | This feature can be used to post for instance a url to the deployed application or code coverage at a successful build and why the build failed like what tests that did not pass. 129 | 130 | 131 | ## License 132 | 133 | - BSD License 134 | 135 | >Copyright (c) 2015, Nathan McCarthy 136 | >All rights reserved. 137 | > 138 | >Redistribution and use in source and binary forms, with or without 139 | >modification, are permitted provided that the following conditions are met: 140 | > * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 141 | > * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 142 | > * Neither the name of the organization nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 143 | > 144 | >THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Nathan McCarthy BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 145 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | org.jenkins-ci.plugins 5 | plugin 6 | 2.7 7 | 8 | 9 | 10 | 11 | 12 | 13 | stash-pullrequest-builder 14 | Stash Pullrequest Builder Plugin 15 | 1.7.1-SNAPSHOT 16 | This Jenkins plugin builds pull requests from Stash and will report the test results. 17 | hpi 18 | https://wiki.jenkins-ci.org/display/JENKINS/Stash+pullrequest+builder+plugin 19 | 20 | 21 | scm:git:ssh://github.com/jenkinsci/stash-pullrequest-builder-plugin.git 22 | scm:git:ssh://git@github.com/jenkinsci/stash-pullrequest-builder-plugin.git 23 | https://github.com/jenkinsci/stash-pullrequest-builder-plugin 24 | HEAD 25 | 26 | 27 | 28 | 29 | nemccarthy 30 | Nathan McCarthy 31 | nem@nemccarthy.me 32 | 33 | 34 | 35 | 36 | 37 | 38 | repo.jenkins-ci.org 39 | http://repo.jenkins-ci.org/public/ 40 | 41 | 42 | 43 | 44 | 45 | org.apache.httpcomponents 46 | httpclient 47 | 4.5.2 48 | 49 | 50 | commons-codec 51 | commons-codec 52 | 1.10 53 | 54 | 55 | org.codehaus.jackson 56 | jackson-jaxrs 57 | 1.9.13 58 | 59 | 60 | org.jenkins-ci.plugins 61 | git 62 | 3.0.0 63 | 64 | 65 | org.jenkins-ci.plugins 66 | credentials 67 | 2.1.5 68 | 69 | 70 | 71 | 72 | 73 | repo.jenkins-ci.org 74 | http://repo.jenkins-ci.org/public/ 75 | 76 | 77 | 78 | 79 | 80 | 81 | org.apache.maven.plugins 82 | maven-release-plugin 83 | 2.5.1 84 | 85 | 86 | org.apache.maven.scm 87 | maven-scm-provider-gitexe 88 | 1.9.2 89 | 90 | 91 | 92 | 93 | org.apache.maven.plugins 94 | maven-compiler-plugin 95 | 3.3 96 | 97 | 1.6 98 | 1.6 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashAditionalParameterEnvironmentContributor.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder; 2 | 3 | import hudson.EnvVars; 4 | import hudson.Extension; 5 | import hudson.model.*; 6 | 7 | import javax.annotation.Nonnull; 8 | import java.io.IOException; 9 | import java.util.*; 10 | 11 | @Extension 12 | public class StashAditionalParameterEnvironmentContributor extends EnvironmentContributor { 13 | private static Set params = 14 | new HashSet(Arrays.asList("sourceBranch", 15 | "targetBranch", 16 | "sourceRepositoryOwner", 17 | "sourceRepositoryName", 18 | "pullRequestId", 19 | "destinationRepositoryOwner", 20 | "destinationRepositoryName", 21 | "pullRequestTitle", 22 | "sourceCommitHash", 23 | "destinationCommitHash")); 24 | 25 | @Override 26 | public void buildEnvironmentFor(@Nonnull Run r, @Nonnull EnvVars envs, @Nonnull TaskListener listener) throws IOException, InterruptedException { 27 | StashCause cause = (StashCause) r.getCause(StashCause.class); 28 | if (cause == null) { 29 | return; 30 | } 31 | ParametersAction pa = r.getAction(ParametersAction.class); 32 | for (String param : params) { 33 | addParameter(param, pa, envs); 34 | } 35 | super.buildEnvironmentFor(r, envs, listener); 36 | } 37 | 38 | private static void addParameter(String key, 39 | ParametersAction pa, 40 | EnvVars envs) { 41 | ParameterValue pv = pa.getParameter(key); 42 | if (pv == null || !(pv instanceof StringParameterValue)) { 43 | return; 44 | } 45 | StringParameterValue value = (StringParameterValue) pa.getParameter(key); 46 | envs.put(key, getString(value.value, "")); 47 | } 48 | 49 | private static String getString(String actual, 50 | String d) { 51 | return actual == null ? d : actual; 52 | } 53 | 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashBuildListener.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder; 2 | 3 | import hudson.Extension; 4 | import hudson.model.Run; 5 | import hudson.model.TaskListener; 6 | import hudson.model.listeners.RunListener; 7 | 8 | import javax.annotation.Nonnull; 9 | import java.util.logging.Logger; 10 | 11 | /** 12 | * Created by Nathan McCarthy 13 | */ 14 | @Extension 15 | public class StashBuildListener extends RunListener> { 16 | private static final Logger logger = Logger.getLogger(StashBuildTrigger.class.getName()); 17 | 18 | @Override 19 | public void onStarted(Run run, TaskListener listener) { 20 | logger.info("BuildListener onStarted called."); 21 | StashBuildTrigger trigger = StashBuildTrigger.getTrigger(run.getParent()); 22 | if (trigger == null) { 23 | return; 24 | } 25 | trigger.getBuilder().getBuilds().onStarted(run); 26 | } 27 | 28 | @Override 29 | public void onCompleted(Run run, @Nonnull TaskListener listener) { 30 | StashBuildTrigger trigger = StashBuildTrigger.getTrigger(run.getParent()); 31 | if (trigger == null) { 32 | return; 33 | } 34 | trigger.getBuilder().getBuilds().onCompleted(run, listener); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashBuildTrigger.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder; 2 | 3 | import antlr.ANTLRException; 4 | import com.cloudbees.plugins.credentials.CredentialsMatchers; 5 | import com.cloudbees.plugins.credentials.CredentialsProvider; 6 | import com.cloudbees.plugins.credentials.common.StandardUsernameListBoxModel; 7 | import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; 8 | import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; 9 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 10 | import hudson.Extension; 11 | import hudson.model.Build; 12 | import hudson.model.Cause; 13 | import hudson.model.CauseAction; 14 | import hudson.model.Item; 15 | import hudson.model.Job; 16 | import hudson.model.ParameterDefinition; 17 | import hudson.model.ParameterValue; 18 | import hudson.model.ParametersAction; 19 | import hudson.model.ParametersDefinitionProperty; 20 | import hudson.model.Queue; 21 | import hudson.model.Result; 22 | import hudson.model.StringParameterValue; 23 | import hudson.model.queue.QueueTaskFuture; 24 | import hudson.model.queue.Tasks; 25 | import hudson.security.ACL; 26 | import hudson.triggers.Trigger; 27 | import hudson.triggers.TriggerDescriptor; 28 | import hudson.util.ListBoxModel; 29 | import jenkins.model.Jenkins; 30 | import jenkins.model.ParameterizedJobMixIn; 31 | import net.sf.json.JSONObject; 32 | import org.acegisecurity.Authentication; 33 | import org.apache.commons.lang.StringUtils; 34 | import org.kohsuke.stapler.AncestorInPath; 35 | import org.kohsuke.stapler.DataBoundConstructor; 36 | import org.kohsuke.stapler.QueryParameter; 37 | import org.kohsuke.stapler.StaplerRequest; 38 | 39 | import javax.annotation.Nonnull; 40 | import javax.annotation.Nullable; 41 | import java.util.ArrayList; 42 | import java.util.List; 43 | import java.util.Map; 44 | import java.util.logging.Level; 45 | import java.util.logging.Logger; 46 | 47 | import static java.lang.String.format; 48 | 49 | /** 50 | * Created by Nathan McCarthy 51 | */ 52 | @SuppressFBWarnings({"WMI_WRONG_MAP_ITERATOR", "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"}) 53 | public class StashBuildTrigger extends Trigger> { 54 | private static final Logger logger = Logger.getLogger(StashBuildTrigger.class.getName()); 55 | private final String projectPath; 56 | private final String cron; 57 | private final String stashHost; 58 | private final String credentialsId; 59 | private final String projectCode; 60 | private final String repositoryName; 61 | private final String ciSkipPhrases; 62 | private final String ciBuildPhrases; 63 | private final String targetBranchesToBuild; 64 | private final boolean ignoreSsl; 65 | private final boolean checkDestinationCommit; 66 | private final boolean checkMergeable; 67 | private final boolean mergeOnSuccess; 68 | private final boolean checkNotConflicted; 69 | private final boolean onlyBuildOnComment; 70 | private final boolean deletePreviousBuildFinishComments; 71 | private final boolean cancelOutdatedJobsEnabled; 72 | 73 | transient private StashPullRequestsBuilder stashPullRequestsBuilder; 74 | 75 | @Extension 76 | public static final StashBuildTriggerDescriptor descriptor = new StashBuildTriggerDescriptor(); 77 | 78 | @DataBoundConstructor 79 | public StashBuildTrigger( 80 | String projectPath, 81 | String cron, 82 | String stashHost, 83 | String credentialsId, 84 | String projectCode, 85 | String repositoryName, 86 | String ciSkipPhrases, 87 | boolean ignoreSsl, 88 | boolean checkDestinationCommit, 89 | boolean checkMergeable, 90 | boolean mergeOnSuccess, 91 | boolean checkNotConflicted, 92 | boolean onlyBuildOnComment, 93 | String ciBuildPhrases, 94 | boolean deletePreviousBuildFinishComments, 95 | String targetBranchesToBuild, 96 | boolean cancelOutdatedJobsEnabled 97 | ) throws ANTLRException { 98 | super(cron); 99 | this.projectPath = projectPath; 100 | this.cron = cron; 101 | this.stashHost = stashHost; 102 | this.credentialsId = credentialsId; 103 | this.projectCode = projectCode; 104 | this.repositoryName = repositoryName; 105 | this.ciSkipPhrases = ciSkipPhrases; 106 | this.cancelOutdatedJobsEnabled = cancelOutdatedJobsEnabled; 107 | this.ciBuildPhrases = ciBuildPhrases == null ? "test this please" : ciBuildPhrases; 108 | this.ignoreSsl = ignoreSsl; 109 | this.checkDestinationCommit = checkDestinationCommit; 110 | this.checkMergeable = checkMergeable; 111 | this.mergeOnSuccess = mergeOnSuccess; 112 | this.checkNotConflicted = checkNotConflicted; 113 | this.onlyBuildOnComment = onlyBuildOnComment; 114 | this.deletePreviousBuildFinishComments = deletePreviousBuildFinishComments; 115 | this.targetBranchesToBuild = targetBranchesToBuild; 116 | } 117 | 118 | public String getStashHost() { 119 | return stashHost; 120 | } 121 | 122 | public String getProjectPath() { 123 | return this.projectPath; 124 | } 125 | 126 | public String getCron() { 127 | return this.cron; 128 | } 129 | 130 | // Needed for Jelly Config 131 | public String getcredentialsId() { 132 | return this.credentialsId; 133 | } 134 | 135 | private StandardUsernamePasswordCredentials getCredentials() { 136 | Authentication defaultAuth = null; 137 | if (job instanceof Queue.Task) { 138 | defaultAuth = Tasks.getDefaultAuthenticationOf((Queue.Task)this.job); 139 | } 140 | return CredentialsMatchers.firstOrNull( 141 | CredentialsProvider.lookupCredentials( 142 | StandardUsernamePasswordCredentials.class, 143 | this.job, 144 | defaultAuth, 145 | URIRequirementBuilder.fromUri(stashHost).build() 146 | ), 147 | CredentialsMatchers.allOf(CredentialsMatchers.withId(credentialsId))); 148 | } 149 | 150 | public String getUsername() { 151 | return this.getCredentials().getUsername(); 152 | } 153 | 154 | public String getPassword() { 155 | return this.getCredentials().getPassword().getPlainText(); 156 | } 157 | 158 | public String getProjectCode() { 159 | return projectCode; 160 | } 161 | 162 | public String getRepositoryName() { 163 | return repositoryName; 164 | } 165 | 166 | public String getCiSkipPhrases() { 167 | return ciSkipPhrases; 168 | } 169 | 170 | public String getCiBuildPhrases() { 171 | return ciBuildPhrases == null ? "test this please" : ciBuildPhrases; 172 | } 173 | 174 | public boolean getCheckDestinationCommit() { 175 | return checkDestinationCommit; 176 | } 177 | 178 | public boolean isIgnoreSsl() { 179 | return ignoreSsl; 180 | } 181 | 182 | public boolean getDeletePreviousBuildFinishComments() { 183 | return deletePreviousBuildFinishComments; 184 | } 185 | 186 | public String getTargetBranchesToBuild() { 187 | return targetBranchesToBuild; 188 | } 189 | 190 | public boolean getMergeOnSuccess() { 191 | return mergeOnSuccess; 192 | } 193 | 194 | public boolean isCancelOutdatedJobsEnabled() { 195 | return cancelOutdatedJobsEnabled; 196 | } 197 | 198 | @Override 199 | public void start(Job job, boolean newInstance) { 200 | try { 201 | this.stashPullRequestsBuilder = StashPullRequestsBuilder.getBuilder(); 202 | this.stashPullRequestsBuilder.setJob(job); 203 | this.stashPullRequestsBuilder.setTrigger(this); 204 | this.stashPullRequestsBuilder.setupBuilder(); 205 | } catch(IllegalStateException e) { 206 | logger.log(Level.SEVERE, "Can't start trigger", e); 207 | return; 208 | } 209 | super.start(job, newInstance); 210 | } 211 | 212 | public static StashBuildTrigger getTrigger(Job job) { 213 | if (!(job instanceof ParameterizedJobMixIn.ParameterizedJob)) { 214 | return null; 215 | } 216 | 217 | ParameterizedJobMixIn.ParameterizedJob pjob = (ParameterizedJobMixIn.ParameterizedJob) job; 218 | 219 | Trigger trigger = pjob.getTriggers().get(descriptor); 220 | return (StashBuildTrigger)trigger; 221 | } 222 | 223 | public StashPullRequestsBuilder getBuilder() { 224 | return this.stashPullRequestsBuilder; 225 | } 226 | 227 | public QueueTaskFuture startJob(StashCause cause) { 228 | List values = getDefaultParameters(); 229 | values.add(new StringParameterValue("sourceBranch", cause.getSourceBranch())); 230 | values.add(new StringParameterValue("targetBranch", cause.getTargetBranch())); 231 | values.add(new StringParameterValue("sourceRepositoryOwner", cause.getSourceRepositoryOwner())); 232 | values.add(new StringParameterValue("sourceRepositoryName", cause.getSourceRepositoryName())); 233 | values.add(new StringParameterValue("pullRequestId", cause.getPullRequestId())); 234 | values.add(new StringParameterValue("destinationRepositoryOwner", cause.getDestinationRepositoryOwner())); 235 | values.add(new StringParameterValue("destinationRepositoryName", cause.getDestinationRepositoryName())); 236 | values.add(new StringParameterValue("pullRequestTitle", cause.getPullRequestTitle())); 237 | values.add(new StringParameterValue("sourceCommitHash", cause.getSourceCommitHash())); 238 | values.add(new StringParameterValue("destinationCommitHash", cause.getDestinationCommitHash())); 239 | 240 | Map additionalParameters = cause.getAdditionalParameters(); 241 | if(additionalParameters != null){ 242 | for(String parameter : additionalParameters.keySet()){ 243 | values.add(new StringParameterValue(parameter, additionalParameters.get(parameter))); 244 | } 245 | } 246 | 247 | if (isCancelOutdatedJobsEnabled()) { 248 | cancelPreviousJobsInQueueThatMatch(cause); 249 | abortRunningJobsThatMatch(cause); 250 | } 251 | 252 | return new ParameterizedJobMixIn() { 253 | @Override 254 | protected Job asJob() { 255 | return StashBuildTrigger.this.job; 256 | } 257 | }.scheduleBuild2(0, new ParametersAction(values), new CauseAction(cause)); 258 | } 259 | 260 | private void cancelPreviousJobsInQueueThatMatch(@Nonnull StashCause stashCause) { 261 | logger.fine("Looking for queued jobs that match PR ID: " + stashCause.getPullRequestId()); 262 | Queue queue = Jenkins.getInstance().getQueue(); 263 | for (Queue.Item item : queue.getItems()) { 264 | if (hasCauseFromTheSamePullRequest(item.getCauses(), stashCause)) { 265 | logger.info("Canceling item in queue: " + item); 266 | queue.cancel(item); 267 | } 268 | } 269 | } 270 | 271 | private void abortRunningJobsThatMatch(@Nonnull StashCause stashCause) { 272 | logger.fine("Looking for running jobs that match PR ID: " + stashCause.getPullRequestId()); 273 | for (Object o : job.getBuilds()) { 274 | if (o instanceof Build) { 275 | Build build = (Build) o; 276 | if (build.isBuilding() && hasCauseFromTheSamePullRequest(build.getCauses(), stashCause)) { 277 | logger.info("Aborting build: " + build + " since PR is outdated"); 278 | build.getExecutor().interrupt(Result.ABORTED); 279 | } 280 | } 281 | } 282 | } 283 | 284 | private boolean hasCauseFromTheSamePullRequest(@Nullable List causes, @Nullable StashCause pullRequestCause) { 285 | if (causes != null && pullRequestCause != null) { 286 | for (Cause cause : causes) { 287 | if (cause instanceof StashCause) { 288 | StashCause sc = (StashCause) cause; 289 | if (StringUtils.equals(sc.getPullRequestId(), pullRequestCause.getPullRequestId()) && 290 | StringUtils.equals(sc.getSourceRepositoryName(), pullRequestCause.getSourceRepositoryName())) { 291 | return true; 292 | } 293 | } 294 | } 295 | } 296 | return false; 297 | } 298 | 299 | private List getDefaultParameters() { 300 | List values = new ArrayList(); 301 | ParametersDefinitionProperty definitionProperty = this.job.getProperty(ParametersDefinitionProperty.class); 302 | if (definitionProperty != null) { 303 | for (ParameterDefinition definition : definitionProperty.getParameterDefinitions()) { 304 | values.add(definition.getDefaultParameterValue()); 305 | } 306 | } 307 | return values; 308 | } 309 | 310 | @Override 311 | public void run() { 312 | if(!this.getBuilder().getJob().isBuildable()) { 313 | logger.info(format("Build Skip (%s).", getBuilder().getJob().getName())); 314 | } else { 315 | logger.info(format("Build started (%s).", getBuilder().getJob().getName())); 316 | this.stashPullRequestsBuilder.run(); 317 | } 318 | this.getDescriptor().save(); 319 | } 320 | 321 | @Override 322 | public void stop() { 323 | super.stop(); 324 | } 325 | 326 | public boolean isCheckMergeable() { 327 | return checkMergeable; 328 | } 329 | 330 | public boolean isCheckNotConflicted() { 331 | return checkNotConflicted; 332 | } 333 | 334 | public boolean isOnlyBuildOnComment() { 335 | return onlyBuildOnComment; 336 | } 337 | 338 | public static final class StashBuildTriggerDescriptor extends TriggerDescriptor { 339 | public StashBuildTriggerDescriptor() { 340 | load(); 341 | } 342 | 343 | @Override 344 | public boolean isApplicable(Item item) { 345 | return true; 346 | } 347 | 348 | @Override 349 | public String getDisplayName() { 350 | return "Stash Pull Requests Builder"; 351 | } 352 | 353 | @Override 354 | public boolean configure(StaplerRequest req, JSONObject json) throws FormException { 355 | save(); 356 | return super.configure(req, json); 357 | } 358 | 359 | public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Item context, @QueryParameter String source) { 360 | if (context == null || !context.hasPermission(Item.CONFIGURE)) { 361 | return new ListBoxModel(); 362 | } 363 | return new StandardUsernameListBoxModel() 364 | .includeEmptyValue() 365 | .includeAs(context instanceof Queue.Task 366 | ? Tasks.getDefaultAuthenticationOf((Queue.Task) context) 367 | : ACL.SYSTEM, context, StandardUsernamePasswordCredentials.class, 368 | URIRequirementBuilder.fromUri(source).build() 369 | ); 370 | } 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashBuilds.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder; 2 | 3 | import hudson.model.Cause; 4 | import hudson.model.Result; 5 | import hudson.model.Run; 6 | import hudson.model.TaskListener; 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 Nathan McCarthy 15 | */ 16 | public class StashBuilds { 17 | private static final Logger logger = Logger.getLogger(StashBuilds.class.getName()); 18 | private StashBuildTrigger trigger; 19 | private StashRepository repository; 20 | 21 | public StashBuilds(StashBuildTrigger trigger, StashRepository repository) { 22 | this.trigger = trigger; 23 | this.repository = repository; 24 | } 25 | 26 | public StashCause getCause(Run run) { 27 | Cause cause = run.getCause(StashCause.class); 28 | if (cause == null || !(cause instanceof StashCause)) { 29 | return null; 30 | } 31 | return (StashCause) cause; 32 | } 33 | 34 | public void onStarted(Run run) { 35 | StashCause cause = this.getCause(run); 36 | if (cause == null) { 37 | return; 38 | } 39 | try { 40 | run.setDescription(cause.getShortDescription()); 41 | } catch (IOException e) { 42 | logger.log(Level.SEVERE, "Can't update build description", e); 43 | } 44 | } 45 | 46 | public void onCompleted(Run run, TaskListener listener) { 47 | StashCause cause = this.getCause(run); 48 | if (cause == null) { 49 | return; 50 | } 51 | Result result = run.getResult(); 52 | JenkinsLocationConfiguration globalConfig = new JenkinsLocationConfiguration(); 53 | String rootUrl = globalConfig.getUrl(); 54 | String buildUrl = ""; 55 | if (rootUrl == null) { 56 | buildUrl = " PLEASE SET JENKINS ROOT URL FROM GLOBAL CONFIGURATION " + run.getUrl(); 57 | } 58 | else { 59 | buildUrl = rootUrl + run.getUrl(); 60 | } 61 | repository.deletePullRequestComment(cause.getPullRequestId(), cause.getBuildStartCommentId()); 62 | 63 | String additionalComment = ""; 64 | 65 | StashPostBuildCommentAction comments = run.getAction(StashPostBuildCommentAction.class); 66 | if(comments != null) { 67 | String buildComment = result == Result.SUCCESS ? comments.getBuildSuccessfulComment() : comments.getBuildFailedComment(); 68 | 69 | if(buildComment != null && !buildComment.isEmpty()) { 70 | additionalComment = "\n\n" + buildComment; 71 | } 72 | } 73 | String duration = run.getDurationString(); 74 | repository.postFinishedComment(cause.getPullRequestId(), cause.getSourceCommitHash(), 75 | cause.getDestinationCommitHash(), result, buildUrl, 76 | run.getNumber(), additionalComment, duration); 77 | 78 | //Merge PR 79 | StashBuildTrigger trig = StashBuildTrigger.getTrigger(run.getParent()); 80 | if(trig.getMergeOnSuccess() && run.getResult() == Result.SUCCESS) { 81 | boolean mergeStat = repository.mergePullRequest(cause.getPullRequestId(), cause.getPullRequestVersion()); 82 | if(mergeStat == true) 83 | { 84 | String logmsg = "Merged pull request " + cause.getPullRequestId() + "(" + 85 | cause.getSourceBranch() + ") to branch " + cause.getTargetBranch(); 86 | logger.log(Level.INFO, logmsg); 87 | listener.getLogger().println(logmsg); 88 | } 89 | else 90 | { 91 | String logmsg = "Failed to merge pull request " + cause.getPullRequestId() + "(" + 92 | cause.getSourceBranch() + ") to branch " + cause.getTargetBranch() + 93 | " because it's out of date"; 94 | logger.log(Level.INFO, logmsg); 95 | listener.getLogger().println(logmsg); 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashCause.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder; 2 | 3 | import java.util.Map; 4 | 5 | import hudson.model.Cause; 6 | 7 | /** 8 | * Created by Nathan McCarthy 9 | */ 10 | public class StashCause extends Cause { 11 | private final String sourceBranch; 12 | private final String targetBranch; 13 | private final String sourceRepositoryOwner; 14 | private final String sourceRepositoryName; 15 | private final String pullRequestId; 16 | private final String destinationRepositoryOwner; 17 | private final String destinationRepositoryName; 18 | private final String pullRequestTitle; 19 | private final String sourceCommitHash; 20 | private final String destinationCommitHash; 21 | private final String buildStartCommentId; 22 | private final String pullRequestVersion; 23 | private final String stashHost; 24 | private final Map additionalParameters; 25 | 26 | public StashCause(String stashHost, 27 | String sourceBranch, 28 | String targetBranch, 29 | String sourceRepositoryOwner, 30 | String sourceRepositoryName, 31 | String pullRequestId, 32 | String destinationRepositoryOwner, 33 | String destinationRepositoryName, 34 | String pullRequestTitle, 35 | String sourceCommitHash, 36 | String destinationCommitHash, 37 | String buildStartCommentId, 38 | String pullRequestVersion, 39 | Map additionalParameters) { 40 | this.sourceBranch = sourceBranch; 41 | this.targetBranch = targetBranch; 42 | this.sourceRepositoryOwner = sourceRepositoryOwner; 43 | this.sourceRepositoryName = sourceRepositoryName; 44 | this.pullRequestId = pullRequestId; 45 | this.destinationRepositoryOwner = destinationRepositoryOwner; 46 | this.destinationRepositoryName = destinationRepositoryName; 47 | this.pullRequestTitle = pullRequestTitle; 48 | this.sourceCommitHash = sourceCommitHash; 49 | this.destinationCommitHash = destinationCommitHash; 50 | this.buildStartCommentId = buildStartCommentId; 51 | this.pullRequestVersion = pullRequestVersion; 52 | this.stashHost = stashHost.replaceAll("/$", ""); 53 | this.additionalParameters = additionalParameters; 54 | } 55 | 56 | public String getSourceBranch() { 57 | return sourceBranch; 58 | } 59 | public String getTargetBranch() { 60 | return targetBranch; 61 | } 62 | 63 | public String getSourceRepositoryOwner() { 64 | return sourceRepositoryOwner; 65 | } 66 | 67 | public String getSourceRepositoryName() { 68 | return sourceRepositoryName; 69 | } 70 | 71 | public String getPullRequestId() { 72 | return pullRequestId; 73 | } 74 | 75 | public String getPullRequestVersion() { 76 | return pullRequestVersion; 77 | } 78 | 79 | public String getDestinationRepositoryOwner() { 80 | return destinationRepositoryOwner; 81 | } 82 | 83 | public String getDestinationRepositoryName() { 84 | return destinationRepositoryName; 85 | } 86 | 87 | public String getPullRequestTitle() { 88 | return pullRequestTitle; 89 | } 90 | 91 | public String getSourceCommitHash() { return sourceCommitHash; } 92 | 93 | public String getDestinationCommitHash() { return destinationCommitHash; } 94 | 95 | public String getBuildStartCommentId() { return buildStartCommentId; } 96 | 97 | public Map getAdditionalParameters() { return additionalParameters; } 98 | 99 | @Override 100 | public String getShortDescription() { 101 | return "PR #" + this.getPullRequestId() + " " + this.getPullRequestTitle() + " "; 104 | } 105 | } 106 | 107 | -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashPostBuildComment.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder; 2 | 3 | import hudson.Util; 4 | import hudson.Extension; 5 | import hudson.Launcher; 6 | import hudson.model.AbstractBuild; 7 | import hudson.model.AbstractProject; 8 | import hudson.model.BuildListener; 9 | import hudson.model.Result; 10 | import hudson.tasks.BuildStepDescriptor; 11 | import hudson.tasks.BuildStepMonitor; 12 | import hudson.tasks.Notifier; 13 | import hudson.tasks.Publisher; 14 | import net.sf.json.JSONObject; 15 | import org.kohsuke.stapler.DataBoundConstructor; 16 | import org.kohsuke.stapler.StaplerRequest; 17 | 18 | import java.util.logging.Level; 19 | import java.util.logging.Logger; 20 | 21 | // TODO: Make available in Jenkins Pipeline 22 | public class StashPostBuildComment extends Notifier { 23 | private static final Logger logger = Logger.getLogger(StashBuildTrigger.class.getName()); 24 | private String buildSuccessfulComment; 25 | private String buildFailedComment; 26 | 27 | @DataBoundConstructor 28 | public StashPostBuildComment(String buildSuccessfulComment, String buildFailedComment) { 29 | this.buildSuccessfulComment = buildSuccessfulComment; 30 | this.buildFailedComment = buildFailedComment; 31 | } 32 | 33 | public BuildStepMonitor getRequiredMonitorService() { 34 | return BuildStepMonitor.BUILD; 35 | } 36 | 37 | public String getBuildSuccessfulComment() { 38 | return buildSuccessfulComment; 39 | } 40 | 41 | public void setBuildSuccessfulComment(String buildSuccessfulComment) { 42 | this.buildSuccessfulComment = buildSuccessfulComment; 43 | } 44 | 45 | public String getBuildFailedComment() { 46 | return buildFailedComment; 47 | } 48 | 49 | public void setBuildFailedComment(String buildFailedComment) { 50 | this.buildFailedComment = buildFailedComment; 51 | } 52 | 53 | @Override 54 | public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) { 55 | 56 | try { 57 | build.addAction(new StashPostBuildCommentAction( 58 | Util.fixEmptyAndTrim(build.getEnvironment(listener).expand(buildSuccessfulComment)), 59 | Util.fixEmptyAndTrim(build.getEnvironment(listener).expand(buildFailedComment)) 60 | )); 61 | } catch (Exception e) { 62 | logger.log(Level.SEVERE, "Unable to parse comments", e); 63 | listener.finished(Result.FAILURE); 64 | return false; 65 | } 66 | 67 | return true; 68 | } 69 | 70 | @Override 71 | public BuildStepDescriptor getDescriptor() { 72 | return DESCRIPTOR; 73 | } 74 | 75 | @Extension 76 | public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); 77 | 78 | public static class DescriptorImpl extends BuildStepDescriptor { 79 | 80 | public DescriptorImpl() { 81 | super(StashPostBuildComment.class); 82 | } 83 | 84 | @Override 85 | public StashPostBuildComment newInstance(StaplerRequest req, JSONObject formData) throws FormException { 86 | return req.bindJSON(StashPostBuildComment.class, formData); 87 | } 88 | 89 | @Override 90 | public boolean isApplicable(Class jobType) { 91 | // Requires that the Build Trigger is set or else nothing is posted. 92 | // We would like that the Post Build Comment option should not be visible when 93 | // the build trigger is not present. 94 | return true; 95 | } 96 | 97 | @Override 98 | public String getDisplayName() { 99 | return "Stash Pull Request Builder - Post Build Comment"; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashPostBuildCommentAction.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder; 2 | 3 | import hudson.model.InvisibleAction; 4 | 5 | public class StashPostBuildCommentAction extends InvisibleAction { 6 | private final String buildSuccessfulComment; 7 | private final String buildFailedComment; 8 | 9 | public StashPostBuildCommentAction(String buildSuccessfulComment, String buildFailedComment) { 10 | this.buildSuccessfulComment = buildSuccessfulComment; 11 | this.buildFailedComment = buildFailedComment; 12 | } 13 | 14 | public String getBuildSuccessfulComment() { 15 | return this.buildSuccessfulComment; 16 | } 17 | 18 | public String getBuildFailedComment() { 19 | return this.buildFailedComment; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashPullRequestsBuilder.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder; 2 | 3 | import static java.lang.String.format; 4 | 5 | import hudson.model.Job; 6 | import stashpullrequestbuilder.stashpullrequestbuilder.stash.StashPullRequestResponseValue; 7 | 8 | import java.util.Collection; 9 | import java.util.logging.Logger; 10 | 11 | /** 12 | * Created by Nathan McCarthy 13 | */ 14 | public class StashPullRequestsBuilder { 15 | private static final Logger logger = Logger.getLogger(StashBuildTrigger.class.getName()); 16 | private Job job; 17 | private StashBuildTrigger trigger; 18 | private StashRepository repository; 19 | private StashBuilds builds; 20 | 21 | public static StashPullRequestsBuilder getBuilder() { 22 | return new StashPullRequestsBuilder(); 23 | } 24 | 25 | public void stop() { 26 | // TODO? 27 | } 28 | 29 | public void run() { 30 | logger.info(format("Build Start (%s).", job.getName())); 31 | this.repository.init(); 32 | Collection targetPullRequests = this.repository.getTargetPullRequests(); 33 | this.repository.addFutureBuildTasks(targetPullRequests); 34 | } 35 | 36 | public StashPullRequestsBuilder setupBuilder() { 37 | if (this.job == null || this.trigger == null) { 38 | throw new IllegalStateException(); 39 | } 40 | this.repository = new StashRepository(this.trigger.getProjectPath(), this); 41 | this.builds = new StashBuilds(this.trigger, this.repository); 42 | return this; 43 | } 44 | 45 | public void setJob(Job job) { 46 | this.job = job; 47 | } 48 | 49 | public void setTrigger(StashBuildTrigger trigger) { 50 | this.trigger = trigger; 51 | } 52 | 53 | public Job getJob() { 54 | return this.job; 55 | } 56 | 57 | public StashBuildTrigger getTrigger() { 58 | return this.trigger; 59 | } 60 | 61 | public StashBuilds getBuilds() { 62 | return this.builds; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/StashRepository.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder; 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 4 | import hudson.model.Result; 5 | import stashpullrequestbuilder.stashpullrequestbuilder.stash.StashApiClient; 6 | import stashpullrequestbuilder.stashpullrequestbuilder.stash.StashPullRequestComment; 7 | import stashpullrequestbuilder.stashpullrequestbuilder.stash.StashPullRequestMergableResponse; 8 | import stashpullrequestbuilder.stashpullrequestbuilder.stash.StashPullRequestResponseValue; 9 | import stashpullrequestbuilder.stashpullrequestbuilder.stash.StashPullRequestResponseValueRepository; 10 | 11 | import java.util.AbstractMap; 12 | import java.util.ArrayList; 13 | import java.util.Collection; 14 | import java.util.Collections; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.TreeMap; 18 | import java.util.logging.Logger; 19 | import java.util.regex.Matcher; 20 | import java.util.regex.Pattern; 21 | 22 | import static java.lang.String.format; 23 | 24 | /** 25 | * Created by Nathan McCarthy 26 | */ 27 | @SuppressFBWarnings("WMI_WRONG_MAP_ITERATOR") 28 | public class StashRepository { 29 | private static final Logger logger = Logger.getLogger(StashRepository.class.getName()); 30 | public static final String BUILD_START_MARKER = "[*BuildStarted* **%s**] %s into %s"; 31 | public static final String BUILD_FINISH_MARKER = "[*BuildFinished* **%s**] %s into %s"; 32 | 33 | public static final String BUILD_START_REGEX = "\\[\\*BuildStarted\\* \\*\\*%s\\*\\*\\] ([0-9a-fA-F]+) into ([0-9a-fA-F]+)"; 34 | public static final String BUILD_FINISH_REGEX = "\\[\\*BuildFinished\\* \\*\\*%s\\*\\*\\] ([0-9a-fA-F]+) into ([0-9a-fA-F]+)"; 35 | 36 | public static final String BUILD_FINISH_SENTENCE = BUILD_FINISH_MARKER + " %n%n **[%s](%s)** - Build *#%d* which took *%s*"; 37 | public static final String BUILD_START_SENTENCE = BUILD_START_MARKER + " %n%n **[%s](%s)** - Build *#%d*"; 38 | 39 | public static final String BUILD_SUCCESS_COMMENT = "✓ BUILD SUCCESS"; 40 | public static final String BUILD_FAILURE_COMMENT = "✕ BUILD FAILURE"; 41 | public static final String BUILD_RUNNING_COMMENT = "BUILD RUNNING..."; 42 | public static final String BUILD_UNSTABLE_COMMENT = "⁉ BUILD UNSTABLE"; 43 | public static final String BUILD_ABORTED_COMMENT = "‼ BUILD ABORTED"; 44 | public static final String BUILD_NOTBUILT_COMMENT = "✕ BUILD INCOMPLETE"; 45 | 46 | public static final String ADDITIONAL_PARAMETER_REGEX = "^p:(([A-Za-z_0-9])+)=(.*)"; 47 | public static final Pattern ADDITIONAL_PARAMETER_REGEX_PATTERN = Pattern.compile(ADDITIONAL_PARAMETER_REGEX); 48 | 49 | private String projectPath; 50 | private StashPullRequestsBuilder builder; 51 | private StashBuildTrigger trigger; 52 | private StashApiClient client; 53 | 54 | public StashRepository(String projectPath, StashPullRequestsBuilder builder) { 55 | this.projectPath = projectPath; 56 | this.builder = builder; 57 | } 58 | 59 | public void init() { 60 | trigger = this.builder.getTrigger(); 61 | client = new StashApiClient( 62 | trigger.getStashHost(), 63 | trigger.getUsername(), 64 | trigger.getPassword(), 65 | trigger.getProjectCode(), 66 | trigger.getRepositoryName(), 67 | trigger.isIgnoreSsl()); 68 | } 69 | 70 | public Collection getTargetPullRequests() { 71 | logger.info(format("Fetch PullRequests (%s).", builder.getJob().getName())); 72 | List pullRequests = client.getPullRequests(); 73 | List targetPullRequests = new ArrayList(); 74 | for(StashPullRequestResponseValue pullRequest : pullRequests) { 75 | if (isBuildTarget(pullRequest)) { 76 | targetPullRequests.add(pullRequest); 77 | } 78 | } 79 | return targetPullRequests; 80 | } 81 | 82 | public String postBuildStartCommentTo(StashPullRequestResponseValue pullRequest) { 83 | String sourceCommit = pullRequest.getFromRef().getLatestCommit(); 84 | String destinationCommit = pullRequest.getToRef().getLatestCommit(); 85 | String comment = format(BUILD_START_MARKER, builder.getJob().getDisplayName(), sourceCommit, destinationCommit); 86 | StashPullRequestComment commentResponse = this.client.postPullRequestComment(pullRequest.getId(), comment); 87 | return commentResponse.getCommentId().toString(); 88 | } 89 | 90 | public static AbstractMap.SimpleEntry getParameter(String content){ 91 | if(content.isEmpty()){ 92 | return null; 93 | } 94 | Matcher parameterMatcher = ADDITIONAL_PARAMETER_REGEX_PATTERN.matcher(content); 95 | if(parameterMatcher.find(0)){ 96 | String parameterName = parameterMatcher.group(1); 97 | String parameterValue = parameterMatcher.group(3); 98 | return new AbstractMap.SimpleEntry(parameterName, parameterValue); 99 | } 100 | return null; 101 | } 102 | 103 | public static Map getParametersFromContent(String content){ 104 | Map result = new TreeMap(); 105 | String lines[] = content.split("\\r?\\n|\\r"); 106 | for(String line : lines){ 107 | AbstractMap.SimpleEntry parameter = getParameter(line); 108 | if(parameter != null){ 109 | result.put(parameter.getKey(), parameter.getValue()); 110 | } 111 | } 112 | 113 | return result; 114 | } 115 | 116 | public Map getAdditionalParameters(StashPullRequestResponseValue pullRequest){ 117 | StashPullRequestResponseValueRepository destination = pullRequest.getToRef(); 118 | String owner = destination.getRepository().getProjectName(); 119 | String repositoryName = destination.getRepository().getRepositoryName(); 120 | 121 | String id = pullRequest.getId(); 122 | List comments = client.getPullRequestComments(owner, repositoryName, id); 123 | if (comments != null) { 124 | Collections.sort(comments); 125 | // Collections.reverse(comments); 126 | 127 | Map result = new TreeMap(); 128 | 129 | for (StashPullRequestComment comment : comments) { 130 | String content = comment.getText(); 131 | if (content == null || content.isEmpty()) { 132 | continue; 133 | } 134 | 135 | Map parameters = getParametersFromContent(content); 136 | for(String key : parameters.keySet()){ 137 | result.put(key, parameters.get(key)); 138 | } 139 | } 140 | return result; 141 | } 142 | return null; 143 | } 144 | 145 | public void addFutureBuildTasks(Collection pullRequests) { 146 | for(StashPullRequestResponseValue pullRequest : pullRequests) { 147 | Map additionalParameters = getAdditionalParameters(pullRequest); 148 | if (trigger.getDeletePreviousBuildFinishComments()) { 149 | deletePreviousBuildFinishedComments(pullRequest); 150 | } 151 | String commentId = postBuildStartCommentTo(pullRequest); 152 | StashCause cause = new StashCause( 153 | trigger.getStashHost(), 154 | pullRequest.getFromRef().getBranch().getName(), 155 | pullRequest.getToRef().getBranch().getName(), 156 | pullRequest.getFromRef().getRepository().getProjectName(), 157 | pullRequest.getFromRef().getRepository().getRepositoryName(), 158 | pullRequest.getId(), 159 | pullRequest.getToRef().getRepository().getProjectName(), 160 | pullRequest.getToRef().getRepository().getRepositoryName(), 161 | pullRequest.getTitle(), 162 | pullRequest.getFromRef().getLatestCommit(), 163 | pullRequest.getToRef().getLatestCommit(), 164 | commentId, 165 | pullRequest.getVersion(), 166 | additionalParameters); 167 | this.builder.getTrigger().startJob(cause); 168 | 169 | } 170 | } 171 | 172 | public void deletePullRequestComment(String pullRequestId, String commentId) { 173 | this.client.deletePullRequestComment(pullRequestId, commentId); 174 | } 175 | 176 | private String getMessageForBuildResult(Result result) { 177 | String message = BUILD_FAILURE_COMMENT; 178 | if (result == Result.SUCCESS) { 179 | message = BUILD_SUCCESS_COMMENT; 180 | } 181 | if (result == Result.UNSTABLE) { 182 | message = BUILD_UNSTABLE_COMMENT; 183 | } 184 | if (result == Result.ABORTED) { 185 | message = BUILD_ABORTED_COMMENT; 186 | } 187 | if (result == Result.NOT_BUILT) { 188 | message = BUILD_NOTBUILT_COMMENT; 189 | } 190 | return message; 191 | } 192 | 193 | public void postFinishedComment(String pullRequestId, String sourceCommit, String destinationCommit, Result buildResult, String buildUrl, int buildNumber, String additionalComment, String duration) { 194 | String message = getMessageForBuildResult(buildResult); 195 | String comment = format(BUILD_FINISH_SENTENCE, builder.getJob().getDisplayName(), sourceCommit, destinationCommit, message, buildUrl, buildNumber, duration); 196 | 197 | comment = comment.concat(additionalComment); 198 | 199 | this.client.postPullRequestComment(pullRequestId, comment); 200 | } 201 | 202 | public boolean mergePullRequest(String pullRequestId, String version) 203 | { 204 | return this.client.mergePullRequest(pullRequestId, version); 205 | } 206 | 207 | private Boolean isPullRequestMergable(StashPullRequestResponseValue pullRequest) { 208 | if (trigger.isCheckMergeable() || trigger.isCheckNotConflicted()) { 209 | StashPullRequestMergableResponse mergable = client.getPullRequestMergeStatus(pullRequest.getId()); 210 | boolean res = true; 211 | if (trigger.isCheckMergeable()) 212 | res = res && mergable.getCanMerge(); 213 | if (trigger.isCheckNotConflicted()) 214 | res = res && !mergable.getConflicted(); 215 | return res; 216 | } 217 | return true; 218 | } 219 | 220 | private void deletePreviousBuildFinishedComments(StashPullRequestResponseValue pullRequest) { 221 | 222 | StashPullRequestResponseValueRepository destination = pullRequest.getToRef(); 223 | String owner = destination.getRepository().getProjectName(); 224 | String repositoryName = destination.getRepository().getRepositoryName(); 225 | String id = pullRequest.getId(); 226 | 227 | List comments = client.getPullRequestComments(owner, repositoryName, id); 228 | 229 | if (comments != null) { 230 | Collections.sort(comments); 231 | Collections.reverse(comments); 232 | for (StashPullRequestComment comment : comments) { 233 | String content = comment.getText(); 234 | if (content == null || content.isEmpty()) { 235 | continue; 236 | } 237 | 238 | String project_build_finished = format(BUILD_FINISH_REGEX, builder.getJob().getDisplayName()); 239 | Matcher finishMatcher = Pattern.compile(project_build_finished, Pattern.CASE_INSENSITIVE).matcher(content); 240 | 241 | if (finishMatcher.find()) { 242 | deletePullRequestComment(pullRequest.getId(), comment.getCommentId().toString()); 243 | } 244 | } 245 | } 246 | } 247 | 248 | private boolean isBuildTarget(StashPullRequestResponseValue pullRequest) { 249 | 250 | boolean shouldBuild = true; 251 | 252 | if (pullRequest.getState() != null && pullRequest.getState().equals("OPEN")) { 253 | if (isSkipBuild(pullRequest.getTitle())) { 254 | logger.info("Skipping PR: " + pullRequest.getId() + " as title contained skip phrase"); 255 | return false; 256 | } 257 | 258 | if (!isForTargetBranch(pullRequest)) { 259 | logger.info("Skipping PR: " + pullRequest.getId() + " as targeting branch: " + pullRequest.getToRef().getBranch().getName()); 260 | return false; 261 | } 262 | 263 | if(!isPullRequestMergable(pullRequest)) { 264 | logger.info("Skipping PR: " + pullRequest.getId() + " as cannot be merged"); 265 | return false; 266 | } 267 | 268 | boolean isOnlyBuildOnComment = trigger.isOnlyBuildOnComment(); 269 | 270 | if (isOnlyBuildOnComment) { 271 | shouldBuild = false; 272 | } 273 | 274 | String sourceCommit = pullRequest.getFromRef().getLatestCommit(); 275 | 276 | StashPullRequestResponseValueRepository destination = pullRequest.getToRef(); 277 | String owner = destination.getRepository().getProjectName(); 278 | String repositoryName = destination.getRepository().getRepositoryName(); 279 | String destinationCommit = destination.getLatestCommit(); 280 | 281 | String id = pullRequest.getId(); 282 | List comments = client.getPullRequestComments(owner, repositoryName, id); 283 | 284 | if (comments != null) { 285 | Collections.sort(comments); 286 | Collections.reverse(comments); 287 | for (StashPullRequestComment comment : comments) { 288 | String content = comment.getText(); 289 | if (content == null || content.isEmpty()) { 290 | continue; 291 | } 292 | 293 | //These will match any start or finish message -- need to check commits 294 | String escapedBuildName = Pattern.quote(builder.getJob().getDisplayName()); 295 | String project_build_start = String.format(BUILD_START_REGEX, escapedBuildName); 296 | String project_build_finished = String.format(BUILD_FINISH_REGEX, escapedBuildName); 297 | Matcher startMatcher = Pattern.compile(project_build_start, Pattern.CASE_INSENSITIVE).matcher(content); 298 | Matcher finishMatcher = Pattern.compile(project_build_finished, Pattern.CASE_INSENSITIVE).matcher(content); 299 | 300 | if (startMatcher.find() || 301 | finishMatcher.find()) { 302 | //in build only on comment, we should stop parsing comments as soon as a PR builder comment is found. 303 | if(isOnlyBuildOnComment) { 304 | assert !shouldBuild; 305 | break; 306 | } 307 | 308 | String sourceCommitMatch; 309 | String destinationCommitMatch; 310 | 311 | if (startMatcher.find(0)) { 312 | sourceCommitMatch = startMatcher.group(1); 313 | destinationCommitMatch = startMatcher.group(2); 314 | } else { 315 | sourceCommitMatch = finishMatcher.group(1); 316 | destinationCommitMatch = finishMatcher.group(2); 317 | } 318 | 319 | //first check source commit -- if it doesn't match, just move on. If it does, investigate further. 320 | if (sourceCommitMatch.equalsIgnoreCase(sourceCommit)) { 321 | // if we're checking destination commits, and if this doesn't match, then move on. 322 | if (this.trigger.getCheckDestinationCommit() 323 | && (!destinationCommitMatch.equalsIgnoreCase(destinationCommit))) { 324 | continue; 325 | } 326 | 327 | shouldBuild = false; 328 | break; 329 | } 330 | } 331 | 332 | if(isSkipBuild(content)) { 333 | shouldBuild = false; 334 | break; 335 | } if (isPhrasesContain(content, this.trigger.getCiBuildPhrases())) { 336 | shouldBuild = true; 337 | break; 338 | } 339 | } 340 | } 341 | } 342 | if (shouldBuild) { 343 | logger.info("Building PR: " + pullRequest.getId()); 344 | } 345 | return shouldBuild; 346 | } 347 | 348 | private boolean isForTargetBranch(StashPullRequestResponseValue pullRequest) { 349 | String targetBranchesToBuild = this.trigger.getTargetBranchesToBuild(); 350 | if (targetBranchesToBuild !=null && !"".equals(targetBranchesToBuild)) { 351 | String[] branches = targetBranchesToBuild.split(","); 352 | for(String branch : branches) { 353 | if (pullRequest.getToRef().getBranch().getName().matches(branch.trim())) { 354 | return true; 355 | } 356 | } 357 | return false; 358 | } 359 | return true; 360 | } 361 | 362 | private boolean isSkipBuild(String pullRequestContentString) { 363 | String skipPhrases = this.trigger.getCiSkipPhrases(); 364 | if (skipPhrases != null && !"".equals(skipPhrases)) { 365 | String[] phrases = skipPhrases.split(","); 366 | for(String phrase : phrases) { 367 | if (isPhrasesContain(pullRequestContentString, phrase)) { 368 | return true; 369 | } 370 | } 371 | } 372 | return false; 373 | } 374 | 375 | private boolean isPhrasesContain(String text, String phrase) { 376 | return text != null && text.toLowerCase().contains(phrase.trim().toLowerCase()); 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/StashApiClient.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder.stash; 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 4 | import org.apache.commons.io.IOUtils; 5 | import org.apache.http.HttpHost; 6 | import org.apache.http.HttpResponse; 7 | import org.apache.http.HttpStatus; 8 | import org.apache.http.auth.AuthScope; 9 | import org.apache.http.auth.Credentials; 10 | import org.apache.http.auth.UsernamePasswordCredentials; 11 | import org.apache.http.client.AuthCache; 12 | import org.apache.http.client.CredentialsProvider; 13 | import org.apache.http.client.HttpClient; 14 | import org.apache.http.client.config.RequestConfig; 15 | import org.apache.http.client.methods.HttpDelete; 16 | import org.apache.http.client.methods.HttpGet; 17 | import org.apache.http.client.methods.HttpPost; 18 | import org.apache.http.client.protocol.HttpClientContext; 19 | import org.apache.http.conn.ssl.NoopHostnameVerifier; 20 | import org.apache.http.conn.ssl.SSLConnectionSocketFactory; 21 | import org.apache.http.conn.ssl.TrustSelfSignedStrategy; 22 | import org.apache.http.entity.ContentType; 23 | import org.apache.http.entity.StringEntity; 24 | import org.apache.http.impl.auth.BasicScheme; 25 | import org.apache.http.impl.client.BasicAuthCache; 26 | import org.apache.http.impl.client.BasicCredentialsProvider; 27 | import org.apache.http.impl.client.HttpClientBuilder; 28 | import org.apache.http.protocol.HttpContext; 29 | import org.apache.http.ssl.SSLContextBuilder; 30 | import org.codehaus.jackson.map.ObjectMapper; 31 | import org.codehaus.jackson.node.ObjectNode; 32 | 33 | import java.io.IOException; 34 | import java.io.InputStream; 35 | import java.io.StringWriter; 36 | import java.io.UnsupportedEncodingException; 37 | import java.net.URI; 38 | import java.security.KeyManagementException; 39 | import java.security.KeyStoreException; 40 | import java.security.NoSuchAlgorithmException; 41 | import java.util.ArrayList; 42 | import java.util.Collections; 43 | import java.util.List; 44 | import java.util.concurrent.Callable; 45 | import java.util.concurrent.FutureTask; 46 | import java.util.concurrent.TimeUnit; 47 | import java.util.concurrent.TimeoutException; 48 | import java.util.logging.Level; 49 | import java.util.logging.Logger; 50 | 51 | /** 52 | * Created by Nathan McCarthy 53 | */ 54 | @SuppressFBWarnings("EQ_DOESNT_OVERRIDE_EQUALS") 55 | public class StashApiClient { 56 | 57 | private static final int HTTP_REQUEST_TIMEOUT_SECONDS = 60; 58 | private static final int HTTP_CONNECTION_TIMEOUT_SECONDS = 15; 59 | private static final int HTTP_SOCKET_TIMEOUT_SECONDS = 15; 60 | 61 | private static final Logger logger = Logger.getLogger(StashApiClient.class.getName()); 62 | private static final ObjectMapper mapper = new ObjectMapper(); 63 | 64 | private String apiBaseUrl; 65 | 66 | private String project; 67 | private String repositoryName; 68 | private Credentials credentials; 69 | private boolean ignoreSsl; 70 | 71 | 72 | public StashApiClient(String stashHost, String username, String password, String project, String repositoryName, boolean ignoreSsl) { 73 | this.credentials = new UsernamePasswordCredentials(username, password); 74 | this.project = project; 75 | this.repositoryName = repositoryName; 76 | this.apiBaseUrl = stashHost.replaceAll("/$", "") + "/rest/api/1.0/projects/"; 77 | this.ignoreSsl = ignoreSsl; 78 | } 79 | 80 | public List getPullRequests() { 81 | List pullRequestResponseValues = new ArrayList(); 82 | try { 83 | boolean isLastPage = false; 84 | int start = 0; 85 | while (!isLastPage) { 86 | String response = getRequest(pullRequestsPath(start)); 87 | StashPullRequestResponse parsedResponse = parsePullRequestJson(response); 88 | isLastPage = parsedResponse.getIsLastPage(); 89 | if (!isLastPage) { 90 | start = parsedResponse.getNextPageStart(); 91 | } 92 | pullRequestResponseValues.addAll(parsedResponse.getPrValues()); 93 | } 94 | return pullRequestResponseValues; 95 | } catch (IOException e) { 96 | logger.log(Level.WARNING, "invalid pull request response.", e); 97 | } 98 | return Collections.EMPTY_LIST; 99 | } 100 | 101 | public List getPullRequestComments(String projectCode, String commentRepositoryName, 102 | String pullRequestId) { 103 | 104 | try { 105 | boolean isLastPage = false; 106 | int start = 0; 107 | List commentResponses = new ArrayList(); 108 | while (!isLastPage) { 109 | String response = getRequest( 110 | apiBaseUrl + projectCode + "/repos/" + commentRepositoryName + "/pull-requests/" + 111 | pullRequestId + "/activities?start=" + start); 112 | StashPullRequestActivityResponse resp = parseCommentJson(response); 113 | isLastPage = resp.getIsLastPage(); 114 | if (!isLastPage) { 115 | start = resp.getNextPageStart(); 116 | } 117 | commentResponses.add(resp); 118 | } 119 | return extractComments(commentResponses); 120 | } catch (Exception e) { 121 | logger.log(Level.WARNING, "invalid pull request response.", e); 122 | } 123 | return Collections.EMPTY_LIST; 124 | } 125 | 126 | public void deletePullRequestComment(String pullRequestId, String commentId) { 127 | String path = pullRequestPath(pullRequestId) + "/comments/" + commentId + "?version=0"; 128 | deleteRequest(path); 129 | } 130 | 131 | 132 | public StashPullRequestComment postPullRequestComment(String pullRequestId, String comment) { 133 | String path = pullRequestPath(pullRequestId) + "/comments"; 134 | try { 135 | String response = postRequest(path, comment); 136 | return parseSingleCommentJson(response); 137 | 138 | } catch (UnsupportedEncodingException e) { 139 | e.printStackTrace(); 140 | } catch (IOException e) { 141 | logger.log(Level.SEVERE, "Failed to post Stash PR comment " + path + " " + e); 142 | } 143 | return null; 144 | } 145 | 146 | public StashPullRequestMergableResponse getPullRequestMergeStatus(String pullRequestId) { 147 | String path = pullRequestPath(pullRequestId) + "/merge"; 148 | try { 149 | String response = getRequest(path); 150 | return parsePullRequestMergeStatus(response); 151 | 152 | } catch (UnsupportedEncodingException e) { 153 | e.printStackTrace(); 154 | } catch (IOException e) { 155 | logger.log(Level.WARNING, "Failed to get Stash PR Merge Status " + path + " " + e); 156 | } 157 | return null; 158 | } 159 | 160 | public boolean mergePullRequest(String pullRequestId, String version) { 161 | String path = pullRequestPath(pullRequestId) + "/merge?version=" + version; 162 | try { 163 | String response = postRequest(path, null); 164 | return !response.equals(Integer.toString(HttpStatus.SC_CONFLICT)); 165 | 166 | } catch (UnsupportedEncodingException e) { 167 | e.printStackTrace(); 168 | } catch (IOException e) { 169 | logger.log(Level.SEVERE, "Failed to merge Stash PR " + path + " " + e); 170 | } 171 | return false; 172 | } 173 | 174 | private HttpContext gethttpContext(Credentials credentials) { 175 | CredentialsProvider credsProvider = new BasicCredentialsProvider(); 176 | credsProvider.setCredentials(AuthScope.ANY, credentials); 177 | AuthCache authCache = new BasicAuthCache(); 178 | BasicScheme basicAuth = new BasicScheme(); 179 | URI stashUri = URI.create(this.apiBaseUrl); 180 | authCache.put(new HttpHost(stashUri.getHost(), stashUri.getPort(), stashUri.getScheme()), basicAuth); 181 | 182 | HttpClientContext context = HttpClientContext.create(); 183 | context.setCredentialsProvider(credsProvider); 184 | context.setAuthCache(authCache); 185 | 186 | RequestConfig config = RequestConfig.copy(context.getRequestConfig()). 187 | setConnectTimeout(StashApiClient.HTTP_CONNECTION_TIMEOUT_SECONDS * 1000). 188 | setSocketTimeout(StashApiClient.HTTP_SOCKET_TIMEOUT_SECONDS * 1000).build(); 189 | context.setRequestConfig(config); 190 | 191 | return context; 192 | } 193 | 194 | private HttpClient getHttpClient() { 195 | HttpClientBuilder builder = HttpClientBuilder.create().useSystemProperties(); 196 | if (this.ignoreSsl) { 197 | try { 198 | SSLContextBuilder sslContextBuilder = new SSLContextBuilder(); 199 | 200 | sslContextBuilder.loadTrustMaterial(null, new TrustSelfSignedStrategy()); 201 | SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContextBuilder.build(), NoopHostnameVerifier.INSTANCE); 202 | builder.setSSLSocketFactory(sslsf); 203 | } catch (NoSuchAlgorithmException e) { 204 | logger.log(Level.SEVERE, "Failing to setup the SSLConnectionFactory: " + e.toString()); 205 | throw new RuntimeException(e); 206 | } catch (KeyStoreException e) { 207 | logger.log(Level.SEVERE, "Failing to setup the SSLConnectionFactory: " + e.toString()); 208 | throw new RuntimeException(e); 209 | } catch (KeyManagementException e) { 210 | logger.log(Level.SEVERE, "Failing to setup the SSLConnectionFactory: " + e.toString()); 211 | throw new RuntimeException(e); 212 | } 213 | } 214 | return builder.build(); 215 | } 216 | 217 | private String getRequest(String path) { 218 | logger.log(Level.FINEST, "PR-GET-REQUEST:" + path); 219 | HttpClient client = getHttpClient(); 220 | HttpContext context = gethttpContext(credentials); 221 | 222 | HttpGet httpget = new HttpGet(path); 223 | //http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html; section 14.10. 224 | //tells the server that we want it to close the connection when it has sent the response. 225 | //address large amount of close_wait sockets client and fin sockets server side 226 | httpget.addHeader("Connection", "close"); 227 | 228 | String response = null; 229 | FutureTask httpTask = null; 230 | Thread thread; 231 | try { 232 | //Run the http request in a future task so we have the opportunity 233 | //to cancel it if it gets hung up; which is possible if stuck at 234 | //socket native layer. see issue JENKINS-30558 235 | httpTask = new FutureTask(new Callable() { 236 | 237 | private HttpClient client; 238 | private HttpContext context; 239 | private HttpGet httpget; 240 | 241 | @Override 242 | public String call() throws Exception { 243 | HttpResponse httpResponse = client.execute(httpget, context); 244 | int responseCode = httpResponse.getStatusLine().getStatusCode(); 245 | String response = httpResponse.getStatusLine().getReasonPhrase(); 246 | if (!validResponseCode(responseCode)) { 247 | logger.log(Level.SEVERE, "Failing to get response from Stash PR GET" + httpget.getURI().getPath()); 248 | throw new RuntimeException("Didn't get a 200 response from Stash PR GET! Response; '" + 249 | responseCode + "' with message; " + response); 250 | } 251 | InputStream responseBodyAsStream = httpResponse.getEntity().getContent(); 252 | StringWriter stringWriter = new StringWriter(); 253 | IOUtils.copy(responseBodyAsStream, stringWriter, "UTF-8"); 254 | response = stringWriter.toString(); 255 | 256 | return response; 257 | 258 | } 259 | 260 | public Callable init(HttpClient client, HttpGet httpget, HttpContext context) { 261 | this.client = client; 262 | this.context = context; 263 | this.httpget = httpget; 264 | return this; 265 | } 266 | 267 | }.init(client, httpget, context)); 268 | thread = new Thread(httpTask); 269 | thread.start(); 270 | response = httpTask.get((long) StashApiClient.HTTP_REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); 271 | 272 | } catch (TimeoutException e) { 273 | e.printStackTrace(); 274 | httpget.abort(); 275 | throw new RuntimeException(e); 276 | } catch (Exception e) { 277 | e.printStackTrace(); 278 | throw new RuntimeException(e); 279 | } finally { 280 | httpget.releaseConnection(); 281 | } 282 | logger.log(Level.FINEST, "PR-GET-RESPONSE:" + response); 283 | return response; 284 | } 285 | 286 | public void deleteRequest(String path) { 287 | HttpClient client = getHttpClient(); 288 | HttpContext context = gethttpContext(this.credentials); 289 | 290 | HttpDelete httpDelete = new HttpDelete(path); 291 | //http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html; section 14.10. 292 | //tells the server that we want it to close the connection when it has sent the response. 293 | //address large amount of close_wait sockets client and fin sockets server side 294 | httpDelete.setHeader("Connection", "close"); 295 | 296 | int res = -1; 297 | FutureTask httpTask = null; 298 | Thread thread; 299 | 300 | try { 301 | //Run the http request in a future task so we have the opportunity 302 | //to cancel it if it gets hung up; which is possible if stuck at 303 | //socket native layer. see issue JENKINS-30558 304 | httpTask = new FutureTask(new Callable() { 305 | 306 | private HttpClient client; 307 | private HttpContext context; 308 | private HttpDelete httpDelete; 309 | 310 | @Override 311 | public Integer call() throws Exception { 312 | int res = -1; 313 | res = client.execute(httpDelete, context).getStatusLine().getStatusCode(); 314 | return res; 315 | 316 | } 317 | 318 | public Callable init(HttpClient client, HttpDelete httpDelete, HttpContext context) { 319 | this.client = client; 320 | this.httpDelete = httpDelete; 321 | this.context = context; 322 | return this; 323 | } 324 | 325 | }.init(client, httpDelete, context)); 326 | thread = new Thread(httpTask); 327 | thread.start(); 328 | res = httpTask.get((long) StashApiClient.HTTP_REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); 329 | 330 | } catch (TimeoutException e) { 331 | e.printStackTrace(); 332 | httpDelete.abort(); 333 | throw new RuntimeException(e); 334 | } catch (Exception e) { 335 | e.printStackTrace(); 336 | throw new RuntimeException(e); 337 | } finally { 338 | httpDelete.releaseConnection(); 339 | } 340 | 341 | logger.log(Level.FINE, "Delete comment {" + path + "} returned result code; " + res); 342 | } 343 | 344 | private String postRequest(String path, String comment) throws UnsupportedEncodingException { 345 | logger.log(Level.FINEST, "PR-POST-REQUEST:" + path + " with: " + comment); 346 | HttpClient client = getHttpClient(); 347 | HttpContext context = gethttpContext(credentials); 348 | 349 | HttpPost httppost = new HttpPost(path); 350 | //http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html; section 14.10. 351 | //tells the server that we want it to close the connection when it has sent the response. 352 | //address large amount of close_wait sockets client and fin sockets server side 353 | httppost.setHeader("Connection", "close"); 354 | httppost.setHeader("X-Atlassian-Token", "no-check"); //xsrf 355 | 356 | if (comment != null) { 357 | ObjectNode node = mapper.getNodeFactory().objectNode(); 358 | node.put("text", comment); 359 | StringEntity requestEntity = null; 360 | try { 361 | requestEntity = new StringEntity( 362 | mapper.writeValueAsString(node), 363 | ContentType.APPLICATION_JSON); 364 | } catch (IOException e) { 365 | e.printStackTrace(); 366 | } 367 | httppost.setEntity(requestEntity); 368 | } 369 | 370 | String response = ""; 371 | FutureTask httpTask = null; 372 | Thread thread; 373 | 374 | try { 375 | //Run the http request in a future task so we have the opportunity 376 | //to cancel it if it gets hung up; which is possible if stuck at 377 | //socket native layer. see issue JENKINS-30558 378 | httpTask = new FutureTask(new Callable() { 379 | 380 | private HttpClient client; 381 | private HttpContext context; 382 | private HttpPost httppost; 383 | 384 | @Override 385 | public String call() throws Exception { 386 | 387 | HttpResponse httpResponse = client.execute(httppost, context); 388 | int responseCode = httpResponse.getStatusLine().getStatusCode(); 389 | String response = httpResponse.getStatusLine().getReasonPhrase(); 390 | if (!validResponseCode(responseCode)) { 391 | logger.log(Level.SEVERE, "Failing to get response from Stash PR POST" + httppost.getURI().getPath()); 392 | throw new RuntimeException("Didn't get a 200 response from Stash PR POST! Response; '" + 393 | responseCode + "' with message; " + response); 394 | } 395 | InputStream responseBodyAsStream = httpResponse.getEntity().getContent(); 396 | StringWriter stringWriter = new StringWriter(); 397 | IOUtils.copy(responseBodyAsStream, stringWriter, "UTF-8"); 398 | response = stringWriter.toString(); 399 | logger.log(Level.FINEST, "API Request Response: " + response); 400 | 401 | return response; 402 | 403 | } 404 | 405 | public Callable init(HttpClient client, HttpPost httppost, HttpContext context) { 406 | this.client = client; 407 | this.context = context; 408 | this.httppost = httppost; 409 | return this; 410 | } 411 | 412 | }.init(client, httppost, context)); 413 | thread = new Thread(httpTask); 414 | thread.start(); 415 | response = httpTask.get((long) StashApiClient.HTTP_REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); 416 | 417 | } catch (TimeoutException e) { 418 | e.printStackTrace(); 419 | httppost.abort(); 420 | throw new RuntimeException(e); 421 | } catch (Exception e) { 422 | e.printStackTrace(); 423 | throw new RuntimeException(e); 424 | } finally { 425 | httppost.releaseConnection(); 426 | } 427 | 428 | logger.log(Level.FINEST, "PR-POST-RESPONSE:" + response); 429 | 430 | return response; 431 | } 432 | 433 | private boolean validResponseCode(int responseCode) { 434 | return responseCode == HttpStatus.SC_OK || 435 | responseCode == HttpStatus.SC_ACCEPTED || 436 | responseCode == HttpStatus.SC_CREATED || 437 | responseCode == HttpStatus.SC_NO_CONTENT || 438 | responseCode == HttpStatus.SC_RESET_CONTENT; 439 | } 440 | 441 | private StashPullRequestResponse parsePullRequestJson(String response) throws IOException { 442 | StashPullRequestResponse parsedResponse; 443 | parsedResponse = mapper.readValue(response, StashPullRequestResponse.class); 444 | return parsedResponse; 445 | } 446 | 447 | private StashPullRequestActivityResponse parseCommentJson(String response) throws IOException { 448 | StashPullRequestActivityResponse parsedResponse; 449 | parsedResponse = mapper.readValue(response, StashPullRequestActivityResponse.class); 450 | return parsedResponse; 451 | } 452 | 453 | private List extractComments(List responses) { 454 | List comments = new ArrayList(); 455 | for (StashPullRequestActivityResponse parsedResponse : responses) { 456 | for (StashPullRequestActivity a : parsedResponse.getPrValues()) { 457 | if (a != null && a.getComment() != null) comments.add(a.getComment()); 458 | } 459 | } 460 | return comments; 461 | } 462 | 463 | private StashPullRequestComment parseSingleCommentJson(String response) throws IOException { 464 | StashPullRequestComment parsedResponse; 465 | parsedResponse = mapper.readValue( 466 | response, 467 | StashPullRequestComment.class); 468 | return parsedResponse; 469 | } 470 | 471 | protected static StashPullRequestMergableResponse parsePullRequestMergeStatus(String response) throws IOException { 472 | StashPullRequestMergableResponse parsedResponse; 473 | parsedResponse = mapper.readValue( 474 | response, 475 | StashPullRequestMergableResponse.class); 476 | return parsedResponse; 477 | } 478 | 479 | private String pullRequestsPath() { 480 | return apiBaseUrl + this.project + "/repos/" + this.repositoryName + "/pull-requests/"; 481 | } 482 | 483 | private String pullRequestPath(String pullRequestId) { 484 | return pullRequestsPath() + pullRequestId; 485 | } 486 | 487 | private String pullRequestsPath(int start) { 488 | String basePath = pullRequestsPath(); 489 | return basePath.substring(0, basePath.length() - 1) + "?start=" + start; 490 | } 491 | 492 | } 493 | 494 | -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/StashPullRequestActivity.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder.stash; 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 4 | import org.codehaus.jackson.annotate.JsonIgnoreProperties; 5 | 6 | /** 7 | * Created by Nathan on 20/03/2015. 8 | */ 9 | @SuppressFBWarnings("EQ_COMPARETO_USE_OBJECT_EQUALS") 10 | @JsonIgnoreProperties(ignoreUnknown = true) 11 | public class StashPullRequestActivity implements Comparable { 12 | private StashPullRequestComment comment; 13 | 14 | public StashPullRequestComment getComment() { 15 | return comment; 16 | } 17 | 18 | public void setComment(StashPullRequestComment comment) { 19 | this.comment = comment; 20 | } 21 | 22 | public int compareTo(StashPullRequestActivity target) { 23 | if (this.comment == null || target.getComment() == null) { 24 | return -1; 25 | } 26 | int commmentIdThis = this.comment.getCommentId(); 27 | int commmentIdOther = target.getComment().getCommentId(); 28 | 29 | if (commmentIdThis > commmentIdOther) { 30 | return 1; 31 | } else if (commmentIdThis == commmentIdOther) { 32 | return 0; 33 | } else { 34 | return -1; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/StashPullRequestActivityResponse.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder.stash; 2 | 3 | import org.codehaus.jackson.annotate.JsonIgnore; 4 | import org.codehaus.jackson.annotate.JsonIgnoreProperties; 5 | import org.codehaus.jackson.annotate.JsonProperty; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * Created by Nathan McCarthy 11 | */ 12 | @JsonIgnoreProperties(ignoreUnknown = true) 13 | public class StashPullRequestActivityResponse { 14 | @JsonIgnore 15 | private List prValues; 16 | 17 | @JsonProperty("size") 18 | private Integer size;// 19 | 20 | private Boolean isLastPage; 21 | 22 | private Integer nextPageStart; 23 | 24 | @JsonProperty("values") 25 | public List getPrValues() { 26 | return prValues; 27 | } 28 | 29 | @JsonProperty("values") 30 | public void setPrValues(List prValues) { 31 | this.prValues = prValues; 32 | } 33 | 34 | public Integer getSize() { 35 | return size; 36 | } 37 | 38 | public void setSize(Integer size) { 39 | this.size = size; 40 | } 41 | 42 | public Boolean getIsLastPage() { 43 | return isLastPage; 44 | } 45 | 46 | public void setIsLastPage(Boolean isLastPage) { 47 | this.isLastPage = isLastPage; 48 | } 49 | 50 | public Integer getNextPageStart() { 51 | return nextPageStart; 52 | } 53 | 54 | public void setNextPageStart(Integer nextPageStart) { 55 | this.nextPageStart = nextPageStart; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/StashPullRequestComment.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder.stash; 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 4 | import org.codehaus.jackson.annotate.JsonIgnoreProperties; 5 | import org.codehaus.jackson.annotate.JsonProperty; 6 | 7 | /** 8 | * Created by Nathan McCarthy 9 | */ 10 | @SuppressFBWarnings("EQ_COMPARETO_USE_OBJECT_EQUALS") 11 | @JsonIgnoreProperties(ignoreUnknown = true) 12 | public class StashPullRequestComment implements Comparable { 13 | 14 | private Integer commentId;// 15 | private String text; 16 | 17 | 18 | @JsonProperty("id") 19 | public Integer getCommentId() { 20 | return commentId; 21 | } 22 | 23 | @JsonProperty("id") 24 | public void setCommentId(Integer commentId) { 25 | this.commentId = commentId; 26 | } 27 | 28 | public String getText() { 29 | return text; 30 | } 31 | 32 | public void setText(String text) { 33 | this.text = text; 34 | } 35 | 36 | 37 | public int compareTo(StashPullRequestComment target) { 38 | if (this.getCommentId() > target.getCommentId()) { 39 | return 1; 40 | } else if (this.getCommentId().equals(target.getCommentId())) { 41 | return 0; 42 | } else { 43 | return -1; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/StashPullRequestMergableResponse.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder.stash; 2 | 3 | import org.codehaus.jackson.annotate.JsonIgnoreProperties; 4 | 5 | import java.util.ArrayList; 6 | 7 | /** 8 | * If pull request is mergeable 9 | * https://developer.atlassian.com/static/rest/stash/3.9.2/stash-rest.html#idp2785024 10 | * 11 | */ 12 | @JsonIgnoreProperties(ignoreUnknown = true) 13 | public class StashPullRequestMergableResponse { 14 | 15 | private Boolean canMerge; 16 | private Boolean conflicted; 17 | private ArrayList vetoes ; 18 | 19 | public Boolean getCanMerge() { 20 | return canMerge; 21 | } 22 | 23 | public void setCanMerge(Boolean canMerge) { 24 | this.canMerge = canMerge; 25 | } 26 | 27 | public Boolean getConflicted() { 28 | return conflicted; 29 | } 30 | 31 | public void setConflicted(Boolean conflicted) { 32 | this.conflicted = conflicted; 33 | } 34 | 35 | public ArrayList getVetoes() { 36 | return vetoes; 37 | } 38 | 39 | public void setVetoes(ArrayList vetoes) { 40 | this.vetoes = vetoes; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/StashPullRequestMergableVetoMessage.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder.stash; 2 | 3 | import org.codehaus.jackson.annotate.JsonIgnoreProperties; 4 | 5 | /** 6 | * If pull request is mergeable 7 | * https://developer.atlassian.com/static/rest/stash/3.9.2/stash-rest.html#idp2785024 8 | * 9 | */ 10 | @JsonIgnoreProperties(ignoreUnknown = true) 11 | public class StashPullRequestMergableVetoMessage { 12 | 13 | private String summaryMessage; 14 | private String detailedMessage; 15 | 16 | public String getSummaryMessage() { 17 | return summaryMessage; 18 | } 19 | 20 | public void setSummaryMessage(String summaryMessage) { 21 | this.summaryMessage = summaryMessage; 22 | } 23 | 24 | public String getDetailedMessage() { 25 | return detailedMessage; 26 | } 27 | 28 | public void setDetailedMessage(String detailedMessage) { 29 | this.detailedMessage = detailedMessage; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/StashPullRequestResponse.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder.stash; 2 | 3 | import org.codehaus.jackson.annotate.JsonIgnoreProperties; 4 | import org.codehaus.jackson.annotate.JsonProperty; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Created by Nathan McCarthy 10 | */ 11 | @JsonIgnoreProperties(ignoreUnknown = true) 12 | public class StashPullRequestResponse { 13 | // private int pageLength; 14 | private List prValues; 15 | 16 | // private int page; 17 | 18 | private int size;// 19 | 20 | private boolean isLastPage; 21 | 22 | private int nextPageStart; 23 | 24 | // @JsonProperty("pagelen") 25 | // public int getPageLength() { 26 | // return pageLength; 27 | // } 28 | // 29 | // @JsonProperty("pagelen") 30 | // public void setPageLength(int pageLength) { 31 | // this.pageLength = pageLength; 32 | // } 33 | 34 | @JsonProperty("values") 35 | public List getPrValues() { 36 | return prValues; 37 | } 38 | 39 | @JsonProperty("values") 40 | public void setPrValues(List prValues) { 41 | this.prValues = prValues; 42 | } 43 | 44 | // @JsonProperty("page") 45 | // public int getPage() { 46 | // return page; 47 | // } 48 | // 49 | // @JsonProperty("page") 50 | // public void setPage(int page) { 51 | // this.page = page; 52 | // } 53 | 54 | @JsonProperty("size") 55 | public int getSize() { 56 | return size; 57 | } 58 | 59 | @JsonProperty("size") 60 | public void setSize(int size) { 61 | this.size = size; 62 | } 63 | 64 | public boolean getIsLastPage() { 65 | return isLastPage; 66 | } 67 | 68 | public int getNextPageStart() { 69 | return nextPageStart; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/StashPullRequestResponseValue.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder.stash; 2 | 3 | import org.codehaus.jackson.annotate.JsonIgnoreProperties; 4 | import org.codehaus.jackson.annotate.JsonProperty; 5 | 6 | /** 7 | * Created by Nathan McCarthy 8 | */ 9 | @JsonIgnoreProperties(ignoreUnknown = true) 10 | public class StashPullRequestResponseValue { 11 | private String description; // 12 | private Boolean locked; // 13 | 14 | private String title; // 15 | 16 | private StashPullRequestResponseValueRepository toRef; 17 | 18 | private Boolean closed; // 19 | 20 | private StashPullRequestResponseValueRepository fromRef; 21 | 22 | private String state; // 23 | private String createdDate; // 24 | private String updatedDate; // 25 | 26 | private String id; // 27 | 28 | private String version; 29 | 30 | public String getDescription() { 31 | return description; 32 | } 33 | 34 | @JsonProperty("version") 35 | public String getVersion() 36 | { 37 | return version; 38 | } 39 | 40 | @JsonProperty("version") 41 | public void setVersion(String ver) 42 | { 43 | this.version = ver; 44 | } 45 | 46 | public void setDescription(String description) { 47 | this.description = description; 48 | } 49 | 50 | @JsonProperty("locked") 51 | public Boolean getLocked() { 52 | return locked; 53 | } 54 | 55 | @JsonProperty("locked") 56 | public void setLocked(Boolean locked) { 57 | this.locked = locked; 58 | } 59 | 60 | public String getTitle() { 61 | return title; 62 | } 63 | 64 | public void setTitle(String title) { 65 | this.title = title; 66 | } 67 | 68 | public StashPullRequestResponseValueRepository getToRef() { 69 | return toRef; 70 | } 71 | 72 | public void setToRef(StashPullRequestResponseValueRepository toRef) { 73 | this.toRef = toRef; 74 | } 75 | 76 | @JsonProperty("closed") 77 | public Boolean getClosed() { 78 | return closed; 79 | } 80 | 81 | @JsonProperty("closed") 82 | public void setClosed(Boolean closed) { 83 | this.closed = closed; 84 | } 85 | 86 | public StashPullRequestResponseValueRepository getFromRef() { 87 | return fromRef; 88 | } 89 | 90 | public void setFromRef(StashPullRequestResponseValueRepository fromRef) { 91 | this.fromRef = fromRef; 92 | } 93 | 94 | public String getState() { 95 | return state; 96 | } 97 | 98 | public void setState(String state) { 99 | this.state = state; 100 | } 101 | 102 | @JsonProperty("createdDate") 103 | public String getCreatedDate() { 104 | return createdDate; 105 | } 106 | 107 | @JsonProperty("createdDate") 108 | public void setCreatedDate(String createdDate) { 109 | this.createdDate = createdDate; 110 | } 111 | 112 | @JsonProperty("updatedDate") 113 | public String getUpdatedDate() { 114 | return updatedDate; 115 | } 116 | 117 | @JsonProperty("updatedDate") 118 | public void setUpdatedDate(String updatedDate) { 119 | this.updatedDate = updatedDate; 120 | } 121 | 122 | 123 | public String getId() { 124 | return id; 125 | } 126 | 127 | public void setId(String id) { 128 | this.id = id; 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/StashPullRequestResponseValueRepository.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder.stash; 2 | 3 | import org.apache.commons.lang.StringUtils; 4 | import org.codehaus.jackson.annotate.JsonIgnore; 5 | import org.codehaus.jackson.annotate.JsonIgnoreProperties; 6 | import org.codehaus.jackson.annotate.JsonProperty; 7 | 8 | /** 9 | * Created by Nathan McCarthy 10 | */ 11 | @JsonIgnoreProperties(ignoreUnknown = true) 12 | public class StashPullRequestResponseValueRepository { 13 | private static final String REFS_PREFIX = "refs/"; 14 | private static final String HEADS_PREFIX = "heads/"; 15 | private StashPullRequestResponseValueRepositoryRepository repository; 16 | 17 | @JsonIgnore 18 | private StashPullRequestResponseValueRepositoryBranch branch; 19 | 20 | @JsonIgnore 21 | private StashPullRequestResponseValueRepositoryCommit commit; 22 | 23 | private String latestChangeset; 24 | private String id; 25 | private String latestCommit; 26 | 27 | 28 | @JsonProperty("id") 29 | public String getId() { 30 | return id; 31 | } 32 | 33 | @JsonProperty("id") 34 | public void setId(String id) { 35 | this.id = id; 36 | this.branch = new StashPullRequestResponseValueRepositoryBranch(); 37 | this.branch.setName( convertIdToBranchName(id) ); 38 | } 39 | 40 | /** 41 | * Convert a pull request identifier to a branch name. Assumption: A pull request identifier always looks like 42 | * "refs/heads/master". The branch name is without the "refs/heads/" part. 43 | * To be on the save side, this method will check for the "refs/" and the "heads/" and strip them accordingly. 44 | * 45 | * More information about the Stash REST API can be found here: 46 | * https://developer.atlassian.com/stash/docs/latest/ 47 | * 48 | * @param id The unique name of the pull request. 49 | * @return The branch name 50 | */ 51 | private String convertIdToBranchName(String id) { 52 | String branchName = StringUtils.EMPTY; 53 | if(StringUtils.isEmpty(id)){ 54 | return branchName; 55 | } 56 | 57 | branchName = id; 58 | 59 | if(StringUtils.startsWith(branchName, REFS_PREFIX)){ 60 | branchName = StringUtils.removeStart(branchName, REFS_PREFIX); 61 | } 62 | 63 | if(StringUtils.startsWith(branchName, HEADS_PREFIX)){ 64 | branchName = StringUtils.removeStart(branchName, HEADS_PREFIX); 65 | } 66 | 67 | return branchName; 68 | } 69 | 70 | @JsonProperty("latestChangeset") 71 | public String getLatestChangeset() { 72 | return latestChangeset; 73 | } 74 | 75 | @JsonProperty("latestChangeset") 76 | public void setLatestChangeset(String latestChangeset) { //TODO 77 | this.latestChangeset = latestChangeset; 78 | this.commit = new StashPullRequestResponseValueRepositoryCommit(); 79 | this.commit.setHash(latestChangeset); 80 | } 81 | 82 | @JsonProperty("repository") 83 | public StashPullRequestResponseValueRepositoryRepository getRepository() { 84 | return repository; 85 | } 86 | 87 | @JsonProperty("repository") 88 | public void setRepository(StashPullRequestResponseValueRepositoryRepository repository) { 89 | this.repository = repository; 90 | } 91 | 92 | @JsonProperty("branch") 93 | public StashPullRequestResponseValueRepositoryBranch getBranch() { 94 | return branch; 95 | } 96 | 97 | @JsonProperty("branch") 98 | public void setBranch(StashPullRequestResponseValueRepositoryBranch branch) { 99 | this.branch = branch; 100 | } 101 | 102 | public StashPullRequestResponseValueRepositoryCommit getCommit() { 103 | return commit; 104 | } 105 | 106 | public void setCommit(StashPullRequestResponseValueRepositoryCommit commit) { 107 | this.commit = commit; 108 | } 109 | 110 | public String getLatestCommit() { 111 | if(commit != null) { 112 | return commit.getHash(); 113 | } 114 | return latestCommit; 115 | } 116 | 117 | public void setLatestCommit(String latestCommit) { 118 | this.latestCommit = latestCommit; 119 | } 120 | } 121 | 122 | 123 | -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/StashPullRequestResponseValueRepositoryBranch.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder.stash; 2 | 3 | import org.codehaus.jackson.annotate.JsonIgnoreProperties; 4 | 5 | @JsonIgnoreProperties(ignoreUnknown = true) 6 | public class StashPullRequestResponseValueRepositoryBranch { 7 | private String Name; 8 | 9 | public String getName() { 10 | return Name; 11 | } 12 | 13 | public void setName(String name) { 14 | Name = name; 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/StashPullRequestResponseValueRepositoryCommit.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder.stash; 2 | 3 | import org.codehaus.jackson.annotate.JsonIgnoreProperties; 4 | 5 | @JsonIgnoreProperties(ignoreUnknown = true) 6 | public class StashPullRequestResponseValueRepositoryCommit { 7 | private String latestChangeset; 8 | 9 | public String getHash() { 10 | return latestChangeset; 11 | } 12 | 13 | public void setHash(String hash) { 14 | this.latestChangeset = hash; 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/StashPullRequestResponseValueRepositoryProject.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder.stash; 2 | 3 | import org.codehaus.jackson.annotate.JsonIgnoreProperties; 4 | 5 | @JsonIgnoreProperties(ignoreUnknown = true) 6 | public class StashPullRequestResponseValueRepositoryProject { 7 | private String key; 8 | 9 | public String getKey() { 10 | return key; 11 | } 12 | 13 | public void setKey(String key) { 14 | this.key = key; 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/StashPullRequestResponseValueRepositoryRepository.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder.stash; 2 | 3 | import org.codehaus.jackson.annotate.JsonIgnoreProperties; 4 | import org.codehaus.jackson.annotate.JsonProperty; 5 | 6 | @JsonIgnoreProperties(ignoreUnknown = true) 7 | public class StashPullRequestResponseValueRepositoryRepository { 8 | private String slug; 9 | private StashPullRequestResponseValueRepositoryProject project; 10 | 11 | @JsonProperty("project") 12 | public StashPullRequestResponseValueRepositoryProject getRepository() { 13 | return project; 14 | } 15 | 16 | @JsonProperty("project") 17 | public void setRepository(StashPullRequestResponseValueRepositoryProject project) { 18 | this.project = project; 19 | } 20 | 21 | public String getSlug() { 22 | return slug; 23 | } 24 | 25 | public void setSlug(String slug) { 26 | this.slug = slug; 27 | } 28 | 29 | public String getProjectName() { 30 | if (this.project != null && project.getKey() != null) { 31 | return project.getKey(); 32 | } 33 | return null; 34 | } 35 | 36 | public String getRepositoryName() { 37 | return this.slug; 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | This plugin polls Atlassian Stash to determine whether there are Pull Requests that should be built. 7 |
8 | -------------------------------------------------------------------------------- /src/main/resources/stashpullrequestbuilder/stashpullrequestbuilder/StashBuildTrigger/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 | -------------------------------------------------------------------------------- /src/main/resources/stashpullrequestbuilder/stashpullrequestbuilder/StashPostBuildComment/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/resources/stashpullrequestbuilder/stashpullrequestbuilder/StashPostBuildComment/help-buildFailedComment.html: -------------------------------------------------------------------------------- 1 |
2 | Comment that will be added to what is posted to Stash when the build failed. Possible to use environment variables like ${BUILD_NUMBER}. 3 |
-------------------------------------------------------------------------------- /src/main/resources/stashpullrequestbuilder/stashpullrequestbuilder/StashPostBuildComment/help-buildSuccessfulComment.html: -------------------------------------------------------------------------------- 1 |
2 | Comment that will be added to what is posted to Stash when the build has been successful. Possible to use environment variables like ${BUILD_NUMBER}. 3 |
-------------------------------------------------------------------------------- /src/test/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/AdditionalParameterRegExTest.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder.stash; 2 | 3 | import org.junit.Test; 4 | import stashpullrequestbuilder.stashpullrequestbuilder.StashRepository; 5 | 6 | import java.util.AbstractMap; 7 | import java.util.Map; 8 | 9 | /** 10 | * Created by nathan on 7/06/2015. 11 | */ 12 | public class AdditionalParameterRegExTest { 13 | 14 | @Test 15 | public void testSingleParameter() throws Exception { 16 | AbstractMap.SimpleEntry test1 = StashRepository.getParameter("p:customer=Nickesocke"); 17 | assert (test1 != null); 18 | assert ("customer".equals(test1.getKey())); 19 | assert ("Nickesocke".equals(test1.getValue())); 20 | } 21 | 22 | @Test 23 | public void testSpacedParameter() throws Exception { 24 | AbstractMap.SimpleEntry test1 = StashRepository.getParameter("p:customer=Nicke socke#"); 25 | assert (test1 != null); 26 | assert ("customer".equals(test1.getKey())); 27 | assert ("Nicke socke#".equals(test1.getValue())); 28 | } 29 | 30 | @Test 31 | public void testBlankParameter() throws Exception { 32 | AbstractMap.SimpleEntry test1 = StashRepository.getParameter("p:the_blank_parameteR="); 33 | assert (test1 != null); 34 | assert ("the_blank_parameteR".equals(test1.getKey())); 35 | assert ("".equals(test1.getValue())); 36 | } 37 | 38 | @Test 39 | public void testInvalidParameter() throws Exception { 40 | AbstractMap.SimpleEntry test1 = StashRepository.getParameter("p:if apa=Nickesocke"); 41 | assert (test1 == null); 42 | AbstractMap.SimpleEntry test2 = StashRepository.getParameter("p:apa==Nickesocke"); 43 | assert (test2 != null && 44 | test2.getKey().equals("apa") && 45 | test2.getValue().equals("=Nickesocke") 46 | ); 47 | AbstractMap.SimpleEntry test3 = StashRepository.getParameter("p:I want to make sure that a use of = will not trigger parameter"); 48 | assert (test3 == null); 49 | AbstractMap.SimpleEntry test4 = StashRepository.getParameter("p:=nothing"); 50 | assert (test4 == null); 51 | AbstractMap.SimpleEntry test5 = StashRepository.getParameter("=nothing"); 52 | assert (test5 == null); 53 | } 54 | 55 | @Test 56 | public void testMultipleParameters() throws Exception { 57 | Map emptyParameters = StashRepository.getParametersFromContent(""); 58 | assert (emptyParameters.isEmpty()); 59 | 60 | Map singleParameters = StashRepository.getParametersFromContent("p:param1=nothing"); 61 | assert (singleParameters.size() == 1); 62 | assert (singleParameters.get("param1").equals("nothing")); 63 | 64 | Map multipleParameters = StashRepository.getParametersFromContent("p:param1=nothing\rp:param2=something special\np:param3=\r\r\n\rjumping to conclusions\r\r\n\n"); 65 | assert (multipleParameters.size() == 3); 66 | assert (multipleParameters.get("param1").equals("nothing")); 67 | assert (multipleParameters.get("param2").equals("something special")); 68 | assert (multipleParameters.get("param3").equals("")); 69 | } 70 | } -------------------------------------------------------------------------------- /src/test/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/StashApiClientTest.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder.stash; 2 | 3 | import org.junit.Test; 4 | 5 | /** 6 | * Created by nathan on 7/06/2015. 7 | */ 8 | public class StashApiClientTest { 9 | 10 | @Test 11 | public void testParsePullRequestMergeStatus() throws Exception { 12 | StashPullRequestMergableResponse resp = StashApiClient.parsePullRequestMergeStatus("{\"canMerge\":false,\"conflicted\":false,\"vetoes\":[{\"summaryMessage\":\"You may not merge after 6pm on a Friday.\",\"detailedMessage\":\"It is likely that your Blood Alcohol Content (BAC) exceeds the threshold for making sensible decisions regarding pull requests. Please try again on Monday.\"}]}"); 13 | assert (resp != null); 14 | assert (!resp.getCanMerge()); 15 | assert (!resp.getConflicted()); 16 | assert (resp.getVetoes().size() == 1); 17 | } 18 | } -------------------------------------------------------------------------------- /src/test/java/stashpullrequestbuilder/stashpullrequestbuilder/stash/StashPullRequestResponseValueRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package stashpullrequestbuilder.stashpullrequestbuilder.stash; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import static org.junit.Assert.assertEquals; 7 | 8 | /** 9 | * Test for checking the logic inside the StashPullRequestResponseValueRepository class. 10 | * Since most of this class consists of getters/setters only one method gets tested. 11 | * 12 | * @author bart 13 | */ 14 | public class StashPullRequestResponseValueRepositoryTest { 15 | 16 | StashPullRequestResponseValueRepository stashPullRequestResponseValueRepository; 17 | 18 | @Before 19 | public void setUp() throws Exception { 20 | stashPullRequestResponseValueRepository = 21 | new StashPullRequestResponseValueRepository(); 22 | } 23 | 24 | @Test 25 | public void testSetIdWithNullValue() throws Exception { 26 | // Test on null values 27 | stashPullRequestResponseValueRepository.setId(null); 28 | String branchName = stashPullRequestResponseValueRepository.getBranch().getName(); 29 | assertEquals("", branchName); 30 | } 31 | 32 | @Test 33 | public void testSetIdWithEmptyValue() throws Exception { 34 | // Test on empty String 35 | stashPullRequestResponseValueRepository.setId(""); 36 | String branchName = stashPullRequestResponseValueRepository.getBranch().getName(); 37 | assertEquals("", branchName); 38 | } 39 | 40 | @Test 41 | public void testSetIdWithSlashInBranchName() throws Exception { 42 | 43 | // Test if the branch name get extracted the right way. Allowing for '/' in the branch name 44 | stashPullRequestResponseValueRepository.setId("refs/heads/release/1.0.0"); 45 | String branchNameWithSlash= stashPullRequestResponseValueRepository.getBranch().getName(); 46 | assertEquals("release/1.0.0", branchNameWithSlash); 47 | } 48 | 49 | @Test 50 | public void testSetIdWithNormalBranchName() throws Exception { 51 | 52 | // Normal case, a branch name without '/' characters 53 | stashPullRequestResponseValueRepository.setId("refs/heads/master"); 54 | String branchNameWithoutSlash = stashPullRequestResponseValueRepository.getBranch().getName(); 55 | assertEquals("master", branchNameWithoutSlash); 56 | 57 | } 58 | 59 | @Test 60 | public void testSetIdWithUnexpectedBranchName() throws Exception { 61 | 62 | // Normal case, but now with a weird pull request identifier 63 | stashPullRequestResponseValueRepository.setId("refs/weird/master"); 64 | String branchNameWithoutSlash = stashPullRequestResponseValueRepository.getBranch().getName(); 65 | assertEquals("weird/master", branchNameWithoutSlash); 66 | 67 | } 68 | } --------------------------------------------------------------------------------