diff options
12 files changed, 771 insertions, 43 deletions
@@ -1,4 +1,4 @@ .idea bitbucket-pullrequest-builder.iml -target +target/ work diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6a2566e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: java + +jdk: + - openjdk7 + - oraclejdk7 + +script: + mvn install -U + +after_failure: + - cat target/surefire-reports/*.txt @@ -3,15 +3,15 @@ <parent> <groupId>org.jenkins-ci.plugins</groupId> <artifactId>plugin</artifactId> - <version>1.509.4</version><!-- which version of Jenkins is this plugin built against? --> + <version>1.509.4</version><!-- which version of Jenkins is this plugin built against? --> </parent> <artifactId>bitbucket-pullrequest-builder</artifactId> <name>Bitbucket Pullrequest Builder Plugin</name> - <version>1.4.9-SNAPSHOT</version> + <version>1.4.13</version> <description>This Jenkins plugin builds pull requests from Bitbucket.org and will report the test results.</description> <packaging>hpi</packaging> - <url>https://wiki.jenkins-ci.org/display/JENKINS/Bitbucket+pullrequest+builder+plugin</url> + <url>https://wiki.jenkins-ci.org/display/JENKINS/Bitbucket+pullrequest+builder+plugin</url> <scm> <connection>scm:git:ssh://git@github.com/jenkinsci/bitbucket-pullrequest-builder-plugin.git</connection> @@ -25,10 +25,10 @@ <name>nishio_dens</name> <email>nishio@densan-labs.net</email> </developer> - </developers> - + </developers> + <!-- get every artifact through repo.jenkins-ci.org, which proxies all the artifacts that we need --> - <repositories> + <repositories> <repository> <id>repo.jenkins-ci.org</id> <url>http://repo.jenkins-ci.org/public/</url> @@ -61,6 +61,17 @@ <artifactId>git</artifactId> <version>2.2.4</version> </dependency> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + <version>14.0-rc3</version> + </dependency> + <dependency> + <groupId>org.easymock</groupId> + <artifactId>easymock</artifactId> + <version>3.4</version> + <scope>test</scope> + </dependency> </dependencies> <pluginRepositories> diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildFilter.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildFilter.java new file mode 100644 index 0000000..c251930 --- /dev/null +++ b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildFilter.java @@ -0,0 +1,184 @@ +package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; + +import java.util.logging.Logger; +import java.util.regex.Pattern; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.logging.Level; +import java.util.regex.Matcher; +import jenkins.plugins.git.AbstractGitSCMSource; +import jenkins.scm.api.SCMSource; + +/** + * Mutable wrapper + */ +class Mutable<T> { + private T value; + public Mutable() { this.value = null; } + public Mutable(T value) { this.value = value; } + T get() { return this.value; } + void set(T value) { this.value = value; } +} + +abstract class Filter { + protected static final Logger logger = Logger.getLogger(BitbucketBuildTrigger.class.getName()); + + public static final String RX_FILTER_FLAG = "r"; + public static final String RX_FILTER_FLAG_SINGLE = RX_FILTER_FLAG + ":"; + + public static final String SRC_RX = "s:(" + RX_FILTER_FLAG_SINGLE + ")?"; + public static final String DST_RX = "d:(" + RX_FILTER_FLAG_SINGLE + ")?"; + public static final String BRANCH_FILTER_RX_PART = "([^\\s$]*)"; + + abstract public boolean apply(String filter, BitbucketCause cause); + abstract public boolean check(String filter); + + static final Pattern RX_SRC_DST_PARTS = Pattern.compile("(s:)|(d:)"); + public static boolean HasSourceOrDestPartsPredicate(String filter) { return RX_SRC_DST_PARTS.matcher(filter).find(); } +} + +class EmptyFilter extends Filter { + @Override + public boolean apply(String filter, BitbucketCause cause) { return true; } + @Override + public boolean check(String filter) { return true; } +} + +class AnyFlag extends Filter { + @Override + public boolean apply(String filter, BitbucketCause cause) { return true; } + @Override + public boolean check(String filter) { return filter.isEmpty() || filter.contains("*") || filter.toLowerCase().contains("any"); } +} + +class OnlySourceFlag extends Filter { + @Override + public boolean apply(String filter, BitbucketCause cause) { + String selectedRx = filter.startsWith(RX_FILTER_FLAG_SINGLE) ? filter.substring(RX_FILTER_FLAG_SINGLE.length()) : Pattern.quote(filter); + logger.log(Level.INFO, "OnlySourceFlag using filter: {0}", selectedRx); + Matcher matcher = Pattern.compile(selectedRx, Pattern.CASE_INSENSITIVE).matcher(cause.getSourceBranch()); + return filter.startsWith(RX_FILTER_FLAG_SINGLE) ? matcher.find() : matcher.matches(); + } + @Override + public boolean check(String filter) { + return false; + } +} + +class OnlyDestFlag extends Filter { + @Override + public boolean apply(String filter, BitbucketCause cause) { + String selectedRx = filter.startsWith(RX_FILTER_FLAG_SINGLE) ? filter.substring(RX_FILTER_FLAG_SINGLE.length()) : Pattern.quote(filter); + logger.log(Level.INFO, "OnlyDestFlag using filter: {0}", selectedRx); + Matcher matcher = Pattern.compile(selectedRx, Pattern.CASE_INSENSITIVE).matcher(cause.getTargetBranch()); + return filter.startsWith(RX_FILTER_FLAG_SINGLE) ? matcher.find() : matcher.matches(); + } + @Override + public boolean check(String filter) { + return !HasSourceOrDestPartsPredicate(filter); + } +} + +class SourceDestFlag extends Filter { + static final Pattern SRC_MATCHER_RX = Pattern.compile(SRC_RX + BRANCH_FILTER_RX_PART, Pattern.CASE_INSENSITIVE | Pattern.CANON_EQ); + static final Pattern DST_MATCHER_RX = Pattern.compile(DST_RX + BRANCH_FILTER_RX_PART, Pattern.CASE_INSENSITIVE | Pattern.CANON_EQ); + + boolean applyByRx(Pattern rx, Filter usedFilter, String filter, BitbucketCause cause) { + Matcher srcMatch = rx.matcher(filter); + boolean apply = rx.matcher(filter).matches(); + while (srcMatch.find()) { + String computedFilter = ((srcMatch.group(1) == null ? "" : srcMatch.group(1)) + srcMatch.group(2)).trim(); + logger.log(Level.INFO, "Apply computed filter: {0}", computedFilter); + apply = apply || (computedFilter.isEmpty() ? true : usedFilter.apply(computedFilter, cause)); + } + return apply; + } + + @Override + public boolean apply(String filter, BitbucketCause cause) { + return this.applyByRx(SRC_MATCHER_RX, new OnlySourceFlag(), filter, cause) && + this.applyByRx(DST_MATCHER_RX, new OnlyDestFlag(), filter, cause); + } + @Override + public boolean check(String filter) { + return HasSourceOrDestPartsPredicate(filter); + } +} + +/** + * Created by maxvodo + */ +public class BitbucketBuildFilter { + private static final Logger logger = Logger.getLogger(BitbucketBuildTrigger.class.getName()); + + private final String filter; + private Filter currFilter = null; + private static final List<Filter> AvailableFilters; + + static { + ArrayList<Filter> filters = new ArrayList<Filter>(); + filters.add(new AnyFlag()); + filters.add(new OnlyDestFlag()); + filters.add(new SourceDestFlag()); + filters.add(new EmptyFilter()); + AvailableFilters = filters; + } + + public BitbucketBuildFilter(String f) { + this.filter = (f != null ? f : "").trim(); + this.buildFilter(this.filter); + } + + private void buildFilter(String filter) { + logger.log(Level.INFO, "Build filter by phrase: {0}", filter); + for(Filter f : AvailableFilters) { + if (f.check(filter)) { + this.currFilter = f; + logger.log(Level.INFO, "Using filter: {0}", f.getClass().getSimpleName()); + break; + } + } + } + + public boolean approved(BitbucketCause cause) { + logger.log(Level.INFO, "Approve cause: {0}", cause.toString()); + return this.currFilter.apply(this.filter, cause); + } + + public static BitbucketBuildFilter InstanceByString(String filter) { + logger.log(Level.INFO, "Filter instance by filter string"); + return new BitbucketBuildFilter(filter); + } + + static public String FilterFromGitSCMSource(AbstractGitSCMSource gitscm, String defaultFilter) { + if (gitscm == null) { + logger.log(Level.INFO, "Git SCMSource unavailable. Using default value: {0}", defaultFilter); + return defaultFilter; + } + + String filter = defaultFilter; + final String includes = gitscm.getIncludes(); + if (includes != null && !includes.isEmpty()) { + for(String part : includes.split("\\s+")) { + filter += String.format("%s ", part.replaceAll("\\*\\/", "d:")); + } + } + + logger.log(Level.INFO, "Git includes transformation to filter result: {1} -> {0}; default: {2}", new Object[]{ filter, includes, defaultFilter }); + return filter.trim(); + } + + public static BitbucketBuildFilter InstanceBySCM(Collection<SCMSource> scmSources, String defaultFilter) { + logger.log(Level.INFO, "Filter instance by using SCMSources list with {0} items", scmSources.size()); + AbstractGitSCMSource gitscm = null; + for(SCMSource scm : scmSources) { + logger.log(Level.INFO, "Check {0} SCMSource ", scm.getClass()); + if (scm instanceof AbstractGitSCMSource) { + gitscm = (AbstractGitSCMSource)scm; + break; + } + } + return new BitbucketBuildFilter(FilterFromGitSCMSource(gitscm, defaultFilter)); + } +} diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger.java index 247bd67..cd9345c 100644 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger.java +++ b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger.java @@ -28,16 +28,18 @@ public class BitbucketBuildTrigger extends Trigger<AbstractProject<?, ?>> { private final String password; private final String repositoryOwner; private final String repositoryName; + private final String branchesFilter; + private final boolean branchesFilterBySCMIncludes; private final String ciKey; private final String ciName; private final String ciSkipPhrases; private final boolean checkDestinationCommit; private final boolean approveIfSuccess; - + transient private BitbucketPullRequestsBuilder bitbucketPullRequestsBuilder; @Extension - public static final BitbucketBuildTriggerDescriptor descriptor = new BitbucketBuildTriggerDescriptor(); + public static final BitbucketBuildTriggerDescriptor descriptor = new BitbucketBuildTriggerDescriptor(); @DataBoundConstructor public BitbucketBuildTrigger( @@ -47,6 +49,8 @@ public class BitbucketBuildTrigger extends Trigger<AbstractProject<?, ?>> { String password, String repositoryOwner, String repositoryName, + String branchesFilter, + boolean branchesFilterBySCMIncludes, String ciKey, String ciName, String ciSkipPhrases, @@ -60,6 +64,8 @@ public class BitbucketBuildTrigger extends Trigger<AbstractProject<?, ?>> { this.password = password; this.repositoryOwner = repositoryOwner; this.repositoryName = repositoryName; + this.branchesFilter = branchesFilter; + this.branchesFilterBySCMIncludes = branchesFilterBySCMIncludes; this.ciKey = ciKey; this.ciName = ciName; this.ciSkipPhrases = ciSkipPhrases; @@ -91,6 +97,14 @@ public class BitbucketBuildTrigger extends Trigger<AbstractProject<?, ?>> { return repositoryName; } + public String getBranchesFilter() { + return branchesFilter; + } + + public boolean getBranchesFilterBySCMIncludes() { + return branchesFilterBySCMIncludes; + } + public String getCiKey() { return ciKey; } @@ -102,7 +116,7 @@ public class BitbucketBuildTrigger extends Trigger<AbstractProject<?, ?>> { public String getCiSkipPhrases() { return ciSkipPhrases; } - + public boolean getCheckDestinationCommit() { return checkDestinationCommit; } diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketPullRequestsBuilder.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketPullRequestsBuilder.java index 3671ea6..c5d4159 100644 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketPullRequestsBuilder.java +++ b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketPullRequestsBuilder.java @@ -2,9 +2,11 @@ package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.Pullrequest; import hudson.model.AbstractProject; +import java.security.MessageDigest; import java.util.Collection; import java.util.logging.Logger; +import org.apache.commons.codec.binary.Hex; /** * Created by nishio @@ -50,6 +52,16 @@ public class BitbucketPullRequestsBuilder { public AbstractProject<?, ?> getProject() { return this.project; + } + + public String getProjectId() { + try { + final MessageDigest MD5 = MessageDigest.getInstance("MD5"); + return new String(Hex.encodeHex(MD5.digest(this.project.getFullName().getBytes("UTF-8")))); + } catch (Exception exc) { + logger.severe(exc.toString()); + } + return this.project.getFullName(); } public BitbucketBuildTrigger getTrigger() { diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketRepository.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketRepository.java index dd0f5e5..6736ee9 100644 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketRepository.java +++ b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketRepository.java @@ -9,7 +9,15 @@ import java.util.logging.Logger; import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.ApiClient; import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.BuildState; import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.Pullrequest; +import java.util.LinkedList; +import java.util.logging.Level; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import jenkins.model.Jenkins; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.SCMSourceOwner; +import jenkins.scm.api.SCMSourceOwners; +import org.apache.commons.lang.StringUtils; /** * Created by nishio @@ -18,6 +26,9 @@ public class BitbucketRepository { private static final Logger logger = Logger.getLogger(BitbucketRepository.class.getName()); private static final String BUILD_DESCRIPTION = "%s: %s into %s"; private static final String BUILD_REQUEST_MARKER = "test this please"; + private static final String BUILD_REQUEST_DONE_MARKER = "ttp build flag"; + private static final String BUILD_REQUEST_MARKER_TAG_SINGLE_RX = "\\#[\\w\\-\\d]+"; + private static final String BUILD_REQUEST_MARKER_TAGS_RX = "\\[bid\\:\\s?(.*)\\]"; private String projectPath; private BitbucketPullRequestsBuilder builder; @@ -30,15 +41,19 @@ public class BitbucketRepository { } public void init() { - trigger = this.builder.getTrigger(); - client = new ApiClient( + this.init(null); + } + + public void init(ApiClient client) { + this.trigger = this.builder.getTrigger(); + this.client = (client == null) ? new ApiClient( trigger.getUsername(), trigger.getPassword(), trigger.getRepositoryOwner(), trigger.getRepositoryName(), trigger.getCiKey(), trigger.getCiName() - ); + ) : client; } public Collection<Pullrequest> getTargetPullRequests() { @@ -87,7 +102,7 @@ public class BitbucketRepository { comment = String.format(BUILD_DESCRIPTION, builder.getProject().getDisplayName(), sourceCommit, destinationBranch); } - this.client.setBuildStatus(owner, repository, sourceCommit, state, buildUrl, comment); + this.client.setBuildStatus(owner, repository, sourceCommit, state, buildUrl, comment, this.builder.getProjectId()); } public void deletePullRequestApproval(String pullRequestId) { @@ -97,10 +112,73 @@ public class BitbucketRepository { public void postPullRequestApproval(String pullRequestId) { this.client.postPullRequestApproval(pullRequestId); } - + + public String getMyBuildTag(String buildKey) { + return "#" + this.client.buildStatusKey(buildKey); + } + + final static Pattern BUILD_TAGS_RX = Pattern.compile(BUILD_REQUEST_MARKER_TAGS_RX, Pattern.CASE_INSENSITIVE | Pattern.CANON_EQ); + final static Pattern SINGLE_BUILD_TAG_RX = Pattern.compile(BUILD_REQUEST_MARKER_TAG_SINGLE_RX, Pattern.CASE_INSENSITIVE | Pattern.CANON_EQ); + final static String CONTENT_PART_TEMPLATE = "```[bid: %s]```"; + + private List<String> getAvailableBuildTagsFromTTPComment(String buildTags) { + logger.log(Level.INFO, "Parse {0}", new Object[]{ buildTags }); + List<String> availableBuildTags = new LinkedList<String>(); + Matcher subBuildTagMatcher = SINGLE_BUILD_TAG_RX.matcher(buildTags); + while(subBuildTagMatcher.find()) availableBuildTags.add(subBuildTagMatcher.group(0).trim()); + return availableBuildTags; + } + + public boolean hasMyBuildTagInTTPComment(String content, String buildKey) { + Matcher tagsMatcher = BUILD_TAGS_RX.matcher(content); + if (tagsMatcher.find()) { + logger.log(Level.INFO, "Content {0} g[1]:{1} mykey:{2}", new Object[] { content, tagsMatcher.group(1).trim(), this.getMyBuildTag(buildKey) }); + return this.getAvailableBuildTagsFromTTPComment(tagsMatcher.group(1).trim()).contains(this.getMyBuildTag(buildKey)); + } + else return false; + } + + private void postBuildTagInTTPComment(String pullRequestId, String content, String buildKey) { + logger.log(Level.INFO, "Update build tag for {0} build key", buildKey); + List<String> builds = this.getAvailableBuildTagsFromTTPComment(content); + builds.add(this.getMyBuildTag(buildKey)); + content += " " + String.format(CONTENT_PART_TEMPLATE, StringUtils.join(builds, " ")); + logger.log(Level.INFO, "Post comment: {0} with original content {1}", new Object[]{ content, this.client.postPullRequestComment(pullRequestId, content).getId() }); + } + + private boolean processTTPCommentBuildTags(String content, String buildKey) { + if (!this.isTTPCommentBuildTags(content)) return true; + logger.log(Level.INFO, "Processing ttp with build comment: {0}", content); + return !this.hasMyBuildTagInTTPComment(content, buildKey); + } + + private boolean isTTPComment(String content) { + return content.toLowerCase().contains(BUILD_REQUEST_MARKER.toLowerCase()); + } + + private boolean isTTPCommentBuildTags(String content) { + return content.toLowerCase().contains(BUILD_REQUEST_DONE_MARKER.toLowerCase()); + } + + public List<Pullrequest.Comment> filterPullRequestComments(List<Pullrequest.Comment> comments) { + logger.info("Filter PullRequest Comments."); + Collections.sort(comments); + Collections.reverse(comments); + List<Pullrequest.Comment> filteredComments = new LinkedList<Pullrequest.Comment>(); + for(Pullrequest.Comment comment : comments) { + String content = comment.getContent(); + if (content == null || content.isEmpty()) continue; + boolean isTTP = this.isTTPComment(content); + boolean isTTPBuild = this.isTTPCommentBuildTags(content); + if (isTTP || isTTPBuild) filteredComments.add(comment); + if (isTTP) break; + } + return filteredComments; + } + private boolean isBuildTarget(Pullrequest pullRequest) { if (pullRequest.getState() != null && pullRequest.getState().equals("OPEN")) { - if (isSkipBuild(pullRequest.getTitle())) { + if (isSkipBuild(pullRequest.getTitle()) || !isFilteredBuild(pullRequest)) { return false; } @@ -110,32 +188,41 @@ public class BitbucketRepository { String owner = destination.getRepository().getOwnerName(); String repositoryName = destination.getRepository().getRepositoryName(); - String id = pullRequest.getId(); + Pullrequest.Repository sourceRepository = source.getRepository(); + String buildKeyPart = this.builder.getProjectId(); + + final boolean commitAlreadyBeenProcessed = this.client.hasBuildStatus( + sourceRepository.getOwnerName(), sourceRepository.getRepositoryName(), sourceCommit, buildKeyPart + ); + if (commitAlreadyBeenProcessed) logger.log(Level.INFO, + "Commit {0}#{1} has already been processed", + new Object[]{ sourceCommit, buildKeyPart } + ); + + final String id = pullRequest.getId(); List<Pullrequest.Comment> comments = client.getPullRequestComments(owner, repositoryName, id); + boolean rebuildCommentAvailable = false; if (comments != null) { - Collections.sort(comments); - Collections.reverse(comments); - for (Pullrequest.Comment comment : comments) { + Collection<Pullrequest.Comment> filteredComments = this.filterPullRequestComments(comments); + for (Pullrequest.Comment comment : filteredComments) { String content = comment.getContent(); - if (content == null || content.isEmpty()) { - continue; - } - - if (content.contains(BUILD_REQUEST_MARKER.toLowerCase())) { - return true; - } + if (this.isTTPComment(content)) { + rebuildCommentAvailable = true; + logger.log(Level.INFO, + "Rebuild comment available for commit {0} and comment #{1}", + new Object[]{ sourceCommit, comment.getId() } + ); + } + rebuildCommentAvailable &= this.processTTPCommentBuildTags(content, buildKeyPart); + if (!rebuildCommentAvailable) break; } - } - - Pullrequest.Repository sourceRepository = source.getRepository(); - - if (this.client.hasBuildStatus(sourceRepository.getOwnerName(), sourceRepository.getRepositoryName(), sourceCommit)) { - logger.info("Commit " + sourceCommit + " has already been processed"); - return false; - } + } + if (rebuildCommentAvailable) this.postBuildTagInTTPComment(id, "TTP build flag", buildKeyPart); - return true; + final boolean canBuildTarget = rebuildCommentAvailable || !commitAlreadyBeenProcessed; + logger.log(Level.INFO, "Build target? {0} [rebuild:{1} processed:{2}]", new Object[]{ canBuildTarget, rebuildCommentAvailable, commitAlreadyBeenProcessed}); + return canBuildTarget; } return false; @@ -153,4 +240,31 @@ public class BitbucketRepository { } return false; } + + private boolean isFilteredBuild(Pullrequest pullRequest) { + BitbucketCause cause = new BitbucketCause( + pullRequest.getSource().getBranch().getName(), + pullRequest.getDestination().getBranch().getName(), + pullRequest.getSource().getRepository().getOwnerName(), + pullRequest.getSource().getRepository().getRepositoryName(), + pullRequest.getId(), + pullRequest.getDestination().getRepository().getOwnerName(), + pullRequest.getDestination().getRepository().getRepositoryName(), + pullRequest.getTitle(), + pullRequest.getSource().getCommit().getHash(), + pullRequest.getDestination().getCommit().getHash() + ); + + //@FIXME: Way to iterate over all available SCMSources + List<SCMSource> sources = new LinkedList<SCMSource>(); + for(SCMSourceOwner owner : SCMSourceOwners.all()) + for(SCMSource src : owner.getSCMSources()) + sources.add(src); + + BitbucketBuildFilter filter = !this.trigger.getBranchesFilterBySCMIncludes() ? + BitbucketBuildFilter.InstanceByString(this.trigger.getBranchesFilter()) : + BitbucketBuildFilter.InstanceBySCM(sources, this.trigger.getBranchesFilter()); + + return filter.approved(cause); + } } diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/ApiClient.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/ApiClient.java index c8f1818..e635f65 100644 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/ApiClient.java +++ b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/bitbucket/ApiClient.java @@ -16,6 +16,9 @@ import java.util.logging.Logger; import jenkins.model.Jenkins; import hudson.ProxyConfiguration; +import org.apache.commons.httpclient.methods.PutMethod; +import org.apache.commons.httpclient.params.HttpMethodParams; +import org.apache.commons.httpclient.util.EncodingUtil; /** * Created by nishio @@ -24,6 +27,7 @@ public class ApiClient { private static final Logger logger = Logger.getLogger(ApiClient.class.getName()); private static final String V1_API_BASE_URL = "https://bitbucket.org/api/1.0/repositories/"; private static final String V2_API_BASE_URL = "https://bitbucket.org/api/2.0/repositories/"; + private static final String COMPUTED_KEY_FORMAT = "%s-%s"; private String owner; private String repositoryName; private Credentials credentials; @@ -55,28 +59,53 @@ public class ApiClient { } return Collections.EMPTY_LIST; } + + public String getName() { + return this.name; + } + + private String computeAPIKey(String keyExPart) { + return String.format(COMPUTED_KEY_FORMAT, this.key, keyExPart); + } + + public String buildStatusKey(String bsKey) { + return this.computeAPIKey(bsKey); + } - public boolean hasBuildStatus(String owner, String repositoryName, String revision) { - String url = v2(owner, repositoryName, "/commit/" + revision + "/statuses/build/" + this.key); + public boolean hasBuildStatus(String owner, String repositoryName, String revision, String keyEx) { + String url = v2(owner, repositoryName, "/commit/" + revision + "/statuses/build/" + this.computeAPIKey(keyEx)); return get(url).contains("\"state\""); } - public void setBuildStatus(String owner, String repositoryName, String revision, BuildState state, String buildUrl, String comment) { + public void setBuildStatus(String owner, String repositoryName, String revision, BuildState state, String buildUrl, String comment, String keyEx) { String url = v2(owner, repositoryName, "/commit/" + revision + "/statuses/build"); + String computedKey = this.computeAPIKey(keyEx); NameValuePair[] data = new NameValuePair[]{ new NameValuePair("description", comment), - new NameValuePair("key", this.key), + new NameValuePair("key", computedKey), new NameValuePair("name", this.name), new NameValuePair("state", state.toString()), new NameValuePair("url", buildUrl), }; - logger.info("POST state " + state + " to " + url); - post(url, data); + logger.log(Level.INFO, "POST state {0} to {1} with key {2} with response {3}", new Object[]{ + state, url, computedKey, post(url, data)} + ); } public void deletePullRequestApproval(String pullRequestId) { delete(v2("/pullrequests/" + pullRequestId + "/approve")); } + + public void deletePullRequestComment(String pullRequestId, String commentId) { + delete(v1("/pullrequests/" + pullRequestId + "/comments/" + commentId)); + } + + public void updatePullRequestComment(String pullRequestId, String content, String commentId) { + NameValuePair[] data = new NameValuePair[] { + new NameValuePair("content", content), + }; + put(v1("/pullrequests/" + pullRequestId + "/comments/" + commentId), data); + } public Pullrequest.Participant postPullRequestApproval(String pullRequestId) { try { @@ -87,6 +116,19 @@ public class ApiClient { } return null; } + + public Pullrequest.Comment postPullRequestComment(String pullRequestId, String content) { + NameValuePair[] data = new NameValuePair[] { + new NameValuePair("content", content), + }; + try { + return parse(post(v1("/pullrequests/" + pullRequestId + "/comments"), data), new TypeReference<Pullrequest.Comment>() {}); + } catch(Exception e) { + logger.log(Level.WARNING, "Invalid pull request comment response.", e); + e.printStackTrace(); + } + return null; + } private HttpClient getHttpClient() { HttpClient client = new HttpClient(); @@ -128,12 +170,20 @@ public class ApiClient { private String post(String path, NameValuePair[] data) { PostMethod req = new PostMethod(path); req.setRequestBody(data); + req.getParams().setContentCharset("utf-8"); return send(req); } private void delete(String path) { send(new DeleteMethod(path)); } + + private void put(String path, NameValuePair[] data) { + PutMethod req = new PutMethod(path); + req.setRequestBody(EncodingUtil.formUrlEncode(data, "utf-8")); + req.getParams().setContentCharset("utf-8"); + send(req); + } private String send(HttpMethodBase req) { HttpClient client = getHttpClient(); diff --git a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/config.jelly b/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/config.jelly index 82ab08c..25acac0 100644 --- a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/config.jelly +++ b/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/config.jelly @@ -14,6 +14,12 @@ <f:entry title="RepositoryName" field="repositoryName"> <f:textbox /> </f:entry> + <f:entry title="BranchesFilter" field="branchesFilter"> + <f:textbox /> + </f:entry> + <f:entry title="Using Git SCM 'Branches to build' option to filter pull requests?" field="branchesFilterBySCMIncludes"> + <f:checkbox /> + </f:entry> <f:entry title="CI Identifier" field="ciKey"> <f:textbox default="jenkins" /> </f:entry> @@ -29,4 +35,4 @@ <f:entry title="Approve if build success?" field="approveIfSuccess"> <f:checkbox /> </f:entry> -</j:jelly>
\ No newline at end of file +</j:jelly> diff --git a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-branchesFilter.html b/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-branchesFilter.html new file mode 100644 index 0000000..88af799 --- /dev/null +++ b/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-branchesFilter.html @@ -0,0 +1,12 @@ +Filter option in custom format. Default value is empty or "any". +Available formats: +* any pull requests applied for this project: "any", "*" or empty string. +* filtered by destination branch: "my-branch" or more complex reg-ex filter "r:^master" (must be started with "r:" and case insensitive match). +* filtered by source and destination branches: "s:source-branch d:dest-branch" +* filtered by source and destination branches with regex: "s:r:^feature d:r:master$" +* filtered by many destination/source branches: "s:one s:two s:three d:master d:r:master$" +* filtered by many sources branches: "s:one s:two s:r:^three d:" +When you using format with source branch filter "s" or destination filter "d", you must cpecify great than one source and destination filter, eg "s:1 s:2 s:... d:". +Any sources and any destinations for pull request: +* filter string: "*" +* filter string: "s: d:" diff --git a/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-branchesFilterBySCMIncludes.html b/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-branchesFilterBySCMIncludes.html new file mode 100644 index 0000000..3f24419 --- /dev/null +++ b/src/main/resources/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildTrigger/help-branchesFilterBySCMIncludes.html @@ -0,0 +1,3 @@ +It is sugar-option for synchronize option "BranchesFilter" to Git SCM option "Branches to build" without manual editing. +Check this option suppose than your "BranchesFilter" field has logick equal value with Git SCM "Branches to build" option (original value from "BranchesFilter" field will e ignored). +If "Branches to build" option has values "*/master */feature-master */build-with-jenkins", then "BranchesFilter" field will have value "d:master d:feature-master d:build-with-jenkins". diff --git a/src/test/java/BitbucketBuildFilterTest.java b/src/test/java/BitbucketBuildFilterTest.java new file mode 100644 index 0000000..0fabfe4 --- /dev/null +++ b/src/test/java/BitbucketBuildFilterTest.java @@ -0,0 +1,311 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketBuildFilter; +import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketCause; +import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketPullRequestsBuilder; +import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.BitbucketRepository; +import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.ApiClient; +import bitbucketpullrequestbuilder.bitbucketpullrequestbuilder.bitbucket.Pullrequest; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Pattern; +import jenkins.plugins.git.AbstractGitSCMSource; +import org.easymock.*; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.WithoutJenkins; + +/** + * + * @author maxvodo + */ +public class BitbucketBuildFilterTest { + + @Rule + public JenkinsRule jRule = new JenkinsRule(); + + @Test + @WithoutJenkins + public void mockTest() { + BitbucketCause cause = EasyMock.createMock(BitbucketCause.class); + EasyMock.expect(cause.getTargetBranch()).andReturn("mock").anyTimes(); + EasyMock.replay(cause); + for(Integer i : new Integer[] {1, 2, 3, 4, 5}) assertEquals("mock", cause.getTargetBranch()); + } + + @Test + @WithoutJenkins + public void anyFilter() { + BitbucketCause cause = EasyMock.createMock(BitbucketCause.class); + EasyMock.expect(cause.getTargetBranch()).andReturn("master").anyTimes(); + EasyMock.replay(cause); + + for(String f : new String[] {"", "*", "any"}) { + BitbucketBuildFilter filter = BitbucketBuildFilter.InstanceByString(f); + assertTrue(filter.approved(cause)); + } + + for(String f : new String[] {"foo", "bar", " baz "}) { + BitbucketBuildFilter filter = BitbucketBuildFilter.InstanceByString(f); + assertFalse(filter.approved(cause)); + } + } + + @Test + @WithoutJenkins + public void onlyDestinationFilter() { + BitbucketCause cause = EasyMock.createMock(BitbucketCause.class); + EasyMock.expect(cause.getTargetBranch()).andReturn("master-branch").anyTimes(); + EasyMock.replay(cause); + + for(String f : new String[] {"master-branch", "r:^master", "r:branch$", " master-branch "}) { + BitbucketBuildFilter filter = BitbucketBuildFilter.InstanceByString(f); + assertTrue(filter.approved(cause)); + } + + for(String f : new String[] {"develop", "feature-good-thing", "r:develop$"}) { + BitbucketBuildFilter filter = BitbucketBuildFilter.InstanceByString(f); + assertFalse(filter.approved(cause)); + } + } + + @Test + @WithoutJenkins + public void rxSourceDestCheck() { + for(String f : new String[] {"", "master", "r:master", "*"}) + assertFalse(Pattern.compile("(s:)|(d:)").matcher(f).find()); + + for(String f : new String[] {"s:master d:feature-master", "s:master d:r:^feature", "s:r:^master d:r:^feature"}) + assertTrue(Pattern.compile("(s:)|(d:)").matcher(f).find()); + } + + @Test + @WithoutJenkins + public void sourceAndDestFilter() { + BitbucketCause cause = EasyMock.createMock(BitbucketCause.class); + EasyMock.expect(cause.getTargetBranch()).andReturn("master").anyTimes(); + EasyMock.expect(cause.getSourceBranch()).andReturn("feature-for-master").anyTimes(); + EasyMock.replay(cause); + + for(String f : new String[] {"s:feature-for-master d:master", "s:r:^feature d:master", "s:feature-for-master d:r:^m", "s:r:^feature d:r:^master"}) { + BitbucketBuildFilter filter = BitbucketBuildFilter.InstanceByString(f); + assertTrue(filter.approved(cause)); + } + + for(String f : new String[] {"s:feature-for-master d:foo", "s:bar d:master", "s:foo d:bar"}) { + BitbucketBuildFilter filter = BitbucketBuildFilter.InstanceByString(f); + assertFalse(filter.approved(cause)); + } + } + + @Test + @WithoutJenkins + public void multipleSrcDestFilter() { + BitbucketCause cause = EasyMock.createMock(BitbucketCause.class); + EasyMock.expect(cause.getTargetBranch()).andReturn("master").anyTimes(); + EasyMock.expect(cause.getSourceBranch()).andReturn("feature-master").anyTimes(); + EasyMock.replay(cause); + + for(String f : new String[] {"s: d:", "s:r:^feature s:good-branch d:r:.*", "s:good-branch s:feature-master d:r:.*", "s: d:r:.*", "d:master d:foo d:bar s:"}) { + BitbucketBuildFilter filter = BitbucketBuildFilter.InstanceByString(f); + assertTrue(filter.approved(cause)); + } + + for(String f : new String[] {"d:ggg d:ooo d:333 s:feature-master", "s:111 s:222 s:333 d:master"}) { + BitbucketBuildFilter filter = BitbucketBuildFilter.InstanceByString(f); + assertFalse(filter.approved(cause)); + } + } + + @Test + @WithoutJenkins + public void sourceAndDestPartiallyFilter() { + BitbucketCause cause = EasyMock.createMock(BitbucketCause.class); + EasyMock.expect(cause.getTargetBranch()).andReturn("master").anyTimes(); + EasyMock.expect(cause.getSourceBranch()).andReturn("feature-master").anyTimes(); + EasyMock.replay(cause); + + for(String f : new String[] {"s:feature-master d:", "d:master s:"}) { + BitbucketBuildFilter filter = BitbucketBuildFilter.InstanceByString(f); + assertTrue(filter.approved(cause)); + } + + for(String f : new String[] {"s:feature-master", "d:master"}) { + BitbucketBuildFilter filter = BitbucketBuildFilter.InstanceByString(f); + assertFalse(filter.approved(cause)); + } + } + + @Test + @WithoutJenkins + public void emptyGitSCMFilter() { + BitbucketCause cause = EasyMock.createMock(BitbucketCause.class); + EasyMock.expect(cause.getTargetBranch()).andReturn("master").anyTimes(); + EasyMock.replay(cause); + + assertTrue(BitbucketBuildFilter.FilterFromGitSCMSource(null, "").isEmpty()); + assertEquals("default", BitbucketBuildFilter.FilterFromGitSCMSource(null, "default")); + + assertTrue(BitbucketBuildFilter.InstanceByString( + BitbucketBuildFilter.FilterFromGitSCMSource(null, "")).approved(cause) + ); + } + + @Test + @WithoutJenkins + public void fromGitSCMFilter() { + AbstractGitSCMSource git = EasyMock.createMock(AbstractGitSCMSource.class); + EasyMock.expect(git.getIncludes()) + .andReturn("").times(1) + .andReturn("").times(1) + .andReturn("*/master */feature-branch").times(1) + .andReturn("*/master").anyTimes(); + EasyMock.replay(git); + + assertTrue(git.getIncludes().isEmpty()); + assertEquals("", BitbucketBuildFilter.FilterFromGitSCMSource(git, "")); + assertEquals("d:master d:feature-branch", BitbucketBuildFilter.FilterFromGitSCMSource(git, "")); + assertEquals("d:master", BitbucketBuildFilter.FilterFromGitSCMSource(git, "")); + } + + @Test + @WithoutJenkins + public void filterPRComments() { + BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); + EasyMock.expect(builder.getTrigger()).andReturn(null).anyTimes(); + EasyMock.replay(builder); + + List<Pullrequest.Comment> comments = new LinkedList<Pullrequest.Comment>(); + for(String commentContent : new String[] { + "check", + "", + "Hello from mock", + "Jenkins: test this please", + "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970]", + "check", + "", + "Hello from mock", + "Jenkins: test this please", + "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970]", + "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970 #jenkins-foo]", + "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970 #jenkins-foo #jenkins-bar]", + }) { + Pullrequest.Comment comment = EasyMock.createNiceMock(Pullrequest.Comment.class); + EasyMock.expect(comment.getContent()).andReturn(commentContent).anyTimes(); + EasyMock.expect(comment.getId()).andReturn(new java.sql.Timestamp(Calendar.getInstance().getTime().getTime()).getNanos()).anyTimes(); + EasyMock.replay(comment); + comments.add(comment); + } + + // Check twice + assertEquals("check", comments.get(0).getContent()); + assertEquals("check", comments.get(0).getContent()); + + assertEquals("Hello from mock", comments.get(2).getContent()); + + BitbucketRepository repo = new BitbucketRepository("", builder); + repo.init(EasyMock.createNiceMock(ApiClient.class)); + + List<Pullrequest.Comment> filteredComments = repo.filterPullRequestComments(comments); + + assertTrue(filteredComments.size() == 4); + assertEquals("Jenkins: test this please", filteredComments.get(filteredComments.size() - 1).getContent()); + } + + @Test + @WithoutJenkins + public void checkHashMyBuildTagTrue() { + BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); + EasyMock.expect(builder.getTrigger()).andReturn(null).anyTimes(); + EasyMock.replay(builder); + + IMockBuilder<BitbucketRepository> repoBuilder = EasyMock.partialMockBuilder(BitbucketRepository.class); + repoBuilder.addMockedMethod("getMyBuildTag"); + BitbucketRepository repo = repoBuilder.createMock(); + EasyMock.expect(repo.getMyBuildTag(EasyMock.anyString())).andReturn("#jenkins-902f259e962ff16100843123480a0970").anyTimes(); + EasyMock.replay(repo); + + List<Pullrequest.Comment> comments = new LinkedList<Pullrequest.Comment>(); + for(String commentContent : new String[] { + "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970]", + "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970 #jenkins-foo]", + "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970 #jenkins-foo #jenkins-bar]", + "TTP build flag ```[bid: #jenkins-902f259e962ff16100843123480a0970 #jenkins-foo #jenkins-bar]```", + }) { + Pullrequest.Comment comment = EasyMock.createNiceMock(Pullrequest.Comment.class); + EasyMock.expect(comment.getContent()).andReturn(commentContent).anyTimes(); + EasyMock.expect(comment.getId()).andReturn(new java.sql.Timestamp(Calendar.getInstance().getTime().getTime()).getNanos()).anyTimes(); + EasyMock.replay(comment); + comments.add(comment); + } + + String myBuildKey = "902f259e962ff16100843123480a0970"; + for(Pullrequest.Comment comment : comments) + assertTrue(repo.hasMyBuildTagInTTPComment(comment.getContent(), myBuildKey)); + } + + @Test + @WithoutJenkins + public void checkHashMyBuildTagFalse() { + BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); + EasyMock.expect(builder.getTrigger()).andReturn(null).anyTimes(); + EasyMock.replay(builder); + + IMockBuilder<BitbucketRepository> repoBuilder = EasyMock.partialMockBuilder(BitbucketRepository.class); + repoBuilder.addMockedMethod("getMyBuildTag"); + BitbucketRepository repo = repoBuilder.createMock(); + EasyMock.expect(repo.getMyBuildTag(EasyMock.anyString())).andReturn("#jenkins-902f259e962ff16100843123480a0970").anyTimes(); + EasyMock.replay(repo); + + List<Pullrequest.Comment> comments = new LinkedList<Pullrequest.Comment>(); + for(String commentContent : new String[] { + "check", + "", + "Hello from mock", + "Jenkins: test this please", + "TTP build flag [bid: #jenkins]", + "TTP build flag [bid: #jenkins-foo]", + "TTP build flag [bid: #jenkins-foo #jenkins-bar]", + "TTP build flag ```[bid: #jenkins-foo #jenkins-bar]```", + }) { + Pullrequest.Comment comment = EasyMock.createNiceMock(Pullrequest.Comment.class); + EasyMock.expect(comment.getContent()).andReturn(commentContent).anyTimes(); + EasyMock.expect(comment.getId()).andReturn(new java.sql.Timestamp(Calendar.getInstance().getTime().getTime()).getNanos()).anyTimes(); + EasyMock.replay(comment); + comments.add(comment); + } + + String myBuildKey = "902f259e962ff16100843123480a0970"; + for(Pullrequest.Comment comment : comments) + assertFalse(repo.hasMyBuildTagInTTPComment(comment.getContent(), myBuildKey)); + } + + //@Test + @WithoutJenkins + public void ttpCommentTest() { + ApiClient client = EasyMock.createNiceMock(ApiClient.class); + Collection<List<Pullrequest>> prs = new LinkedList<List<Pullrequest>>(); + + prs.add(Arrays.asList(new Pullrequest[] { + new Pullrequest() + })); + + for(List<Pullrequest> pr : prs) EasyMock.expect(client.getPullRequests()).andReturn(pr).times(1); + BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); + EasyMock.replay(client, builder); + + BitbucketRepository repo = new BitbucketRepository("", builder); + repo.init(client); + + Collection<Pullrequest> targetPRs = repo.getTargetPullRequests(); + } +} |