From 96ab7a75f14d9990f3c8f1255f9790c496a64473 Mon Sep 17 00:00:00 2001 From: Maxim Epishchev Date: Tue, 26 Jan 2016 19:06:59 +0300 Subject: Bugfixes issue for continuously rebuilding PR If available TTP (aka "test this please") comment Jenkins PR builder continuously rebuilding PR. Now Jenkins post specific build comment. If you want to rebuild already rebuilded PR, post new TTP comment. --- .../BitbucketBuildFilter.java | 13 +- .../BitbucketRepository.java | 127 +++++++++++++++---- .../bitbucket/ApiClient.java | 50 +++++++- src/test/java/BitbucketBuildFilterTest.java | 141 +++++++++++++++++++++ 4 files changed, 295 insertions(+), 36 deletions(-) (limited to 'src') diff --git a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildFilter.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildFilter.java index 1072337..c251930 100644 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildFilter.java +++ b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketBuildFilter.java @@ -1,9 +1,9 @@ package bitbucketpullrequestbuilder.bitbucketpullrequestbuilder; -import hudson.ExtensionList; 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; @@ -169,12 +169,15 @@ public class BitbucketBuildFilter { return filter.trim(); } - public static BitbucketBuildFilter InstanceBySCM(ExtensionList scmSources, String defaultFilter) { - logger.log(Level.FINE, "Filter instance by using SCM"); + public static BitbucketBuildFilter InstanceBySCM(Collection 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) { - gitscm = (AbstractGitSCMSource)scm; - if (gitscm != null) break; + 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/BitbucketRepository.java b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketRepository.java index c71cc99..6736ee9 100644 --- a/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketRepository.java +++ b/src/main/java/bitbucketpullrequestbuilder/bitbucketpullrequestbuilder/BitbucketRepository.java @@ -9,11 +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 @@ -22,7 +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_MARKER_COUNTER_RX = "\\[(\\d+)]\\"; + 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; @@ -35,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 getTargetPullRequests() { @@ -102,10 +112,68 @@ public class BitbucketRepository { public void postPullRequestApproval(String pullRequestId) { this.client.postPullRequestApproval(pullRequestId); } - - private Integer extractRebuildTimesFromComment(String content) { - Matcher matcher = Pattern.compile(BUILD_REQUEST_MARKER_COUNTER_RX).matcher(content); - return matcher.groupCount() >= 1 ? Integer.parseInt(matcher.group(1)) : 0; + + public String getMyBuildTag(String buildKey) { + return "#" + this.client.buildStatusKey(buildKey); + } + + final static Pattern BUILD_TAGS_RX = Pattern.compile(BUILD_REQUEST_MARKER_TAGS_RX, Pattern.CASE_INSENSITIVE | Pattern.CANON_EQ); + final static Pattern SINGLE_BUILD_TAG_RX = Pattern.compile(BUILD_REQUEST_MARKER_TAG_SINGLE_RX, Pattern.CASE_INSENSITIVE | Pattern.CANON_EQ); + final static String CONTENT_PART_TEMPLATE = "```[bid: %s]```"; + + private List getAvailableBuildTagsFromTTPComment(String buildTags) { + logger.log(Level.INFO, "Parse {0}", new Object[]{ buildTags }); + List availableBuildTags = new LinkedList(); + Matcher subBuildTagMatcher = SINGLE_BUILD_TAG_RX.matcher(buildTags); + while(subBuildTagMatcher.find()) availableBuildTags.add(subBuildTagMatcher.group(0).trim()); + return availableBuildTags; + } + + public boolean hasMyBuildTagInTTPComment(String content, String buildKey) { + Matcher tagsMatcher = BUILD_TAGS_RX.matcher(content); + if (tagsMatcher.find()) { + logger.log(Level.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 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 filterPullRequestComments(List comments) { + logger.info("Filter PullRequest Comments."); + Collections.sort(comments); + Collections.reverse(comments); + List filteredComments = new LinkedList(); + for(Pullrequest.Comment comment : comments) { + String content = comment.getContent(); + if (content == null || content.isEmpty()) continue; + boolean isTTP = this.isTTPComment(content); + boolean isTTPBuild = this.isTTPCommentBuildTags(content); + if (isTTP || isTTPBuild) filteredComments.add(comment); + if (isTTP) break; + } + return filteredComments; } private boolean isBuildTarget(Pullrequest pullRequest) { @@ -121,41 +189,40 @@ public class BitbucketRepository { String repositoryName = destination.getRepository().getRepositoryName(); Pullrequest.Repository sourceRepository = source.getRepository(); + String buildKeyPart = this.builder.getProjectId(); - boolean commitAlreadyBeenProcessed = this.client.hasBuildStatus( - sourceRepository.getOwnerName(), sourceRepository.getRepositoryName(), sourceCommit, 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, this.builder.getProjectId() } + new Object[]{ sourceCommit, buildKeyPart } ); - String id = pullRequest.getId(); + final String id = pullRequest.getId(); List comments = client.getPullRequestComments(owner, repositoryName, id); boolean rebuildCommentAvailable = false; if (comments != null) { - Collections.sort(comments); - Collections.reverse(comments); - for (Pullrequest.Comment comment : comments) { + Collection 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())) { + if (this.isTTPComment(content)) { rebuildCommentAvailable = true; logger.log(Level.INFO, "Rebuild comment available for commit {0} and comment #{1}", new Object[]{ sourceCommit, comment.getId() } - ); - this.client.deleteComment(id, Integer.toString(comment.getId())); - } + ); + } + rebuildCommentAvailable &= this.processTTPCommentBuildTags(content, buildKeyPart); + if (!rebuildCommentAvailable) break; } - } + } + if (rebuildCommentAvailable) this.postBuildTagInTTPComment(id, "TTP build flag", buildKeyPart); - logger.log(Level.INFO, "Build target? {0}", rebuildCommentAvailable || !commitAlreadyBeenProcessed); - return rebuildCommentAvailable || !commitAlreadyBeenProcessed; + 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; @@ -188,9 +255,15 @@ public class BitbucketRepository { pullRequest.getDestination().getCommit().getHash() ); + //@FIXME: Way to iterate over all available SCMSources + List sources = new LinkedList(); + for(SCMSourceOwner owner : SCMSourceOwners.all()) + for(SCMSource src : owner.getSCMSources()) + sources.add(src); + BitbucketBuildFilter filter = !this.trigger.getBranchesFilterBySCMIncludes() ? BitbucketBuildFilter.InstanceByString(this.trigger.getBranchesFilter()) : - BitbucketBuildFilter.InstanceBySCM(Jenkins.getInstance().getExtensionList(SCMSource.class), 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 18ca86f..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 @@ -56,15 +59,27 @@ 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 keyEx) { - String url = v2(owner, repositoryName, "/commit/" + revision + "/statuses/build/" + String.format(COMPUTED_KEY_FORMAT, this.key, 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, String keyEx) { String url = v2(owner, repositoryName, "/commit/" + revision + "/statuses/build"); - String computedKey = String.format(COMPUTED_KEY_FORMAT, this.key, keyEx); + String computedKey = this.computeAPIKey(keyEx); NameValuePair[] data = new NameValuePair[]{ new NameValuePair("description", comment), new NameValuePair("key", computedKey), @@ -81,8 +96,15 @@ public class ApiClient { delete(v2("/pullrequests/" + pullRequestId + "/approve")); } - public void deleteComment(String pullRequestId, String commentId) { - delete(v1("/pullrequests/" + pullRequestId + "/comments/" + commentId + "/")); + 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) { @@ -94,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() {}); + } catch(Exception e) { + logger.log(Level.WARNING, "Invalid pull request comment response.", e); + e.printStackTrace(); + } + return null; + } private HttpClient getHttpClient() { HttpClient client = new HttpClient(); @@ -142,6 +177,13 @@ public class ApiClient { 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/test/java/BitbucketBuildFilterTest.java b/src/test/java/BitbucketBuildFilterTest.java index 0765a4a..0fabfe4 100644 --- a/src/test/java/BitbucketBuildFilterTest.java +++ b/src/test/java/BitbucketBuildFilterTest.java @@ -6,6 +6,15 @@ 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.*; @@ -167,4 +176,136 @@ public class BitbucketBuildFilterTest { 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 comments = new LinkedList(); + for(String commentContent : new String[] { + "check", + "", + "Hello from mock", + "Jenkins: test this please", + "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970]", + "check", + "", + "Hello from mock", + "Jenkins: test this please", + "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970]", + "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970 #jenkins-foo]", + "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970 #jenkins-foo #jenkins-bar]", + }) { + Pullrequest.Comment comment = EasyMock.createNiceMock(Pullrequest.Comment.class); + EasyMock.expect(comment.getContent()).andReturn(commentContent).anyTimes(); + EasyMock.expect(comment.getId()).andReturn(new java.sql.Timestamp(Calendar.getInstance().getTime().getTime()).getNanos()).anyTimes(); + EasyMock.replay(comment); + comments.add(comment); + } + + // Check twice + assertEquals("check", comments.get(0).getContent()); + assertEquals("check", comments.get(0).getContent()); + + assertEquals("Hello from mock", comments.get(2).getContent()); + + BitbucketRepository repo = new BitbucketRepository("", builder); + repo.init(EasyMock.createNiceMock(ApiClient.class)); + + List filteredComments = repo.filterPullRequestComments(comments); + + assertTrue(filteredComments.size() == 4); + assertEquals("Jenkins: test this please", filteredComments.get(filteredComments.size() - 1).getContent()); + } + + @Test + @WithoutJenkins + public void checkHashMyBuildTagTrue() { + BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); + EasyMock.expect(builder.getTrigger()).andReturn(null).anyTimes(); + EasyMock.replay(builder); + + IMockBuilder repoBuilder = EasyMock.partialMockBuilder(BitbucketRepository.class); + repoBuilder.addMockedMethod("getMyBuildTag"); + BitbucketRepository repo = repoBuilder.createMock(); + EasyMock.expect(repo.getMyBuildTag(EasyMock.anyString())).andReturn("#jenkins-902f259e962ff16100843123480a0970").anyTimes(); + EasyMock.replay(repo); + + List comments = new LinkedList(); + for(String commentContent : new String[] { + "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970]", + "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970 #jenkins-foo]", + "TTP build flag [bid: #jenkins-902f259e962ff16100843123480a0970 #jenkins-foo #jenkins-bar]", + "TTP build flag ```[bid: #jenkins-902f259e962ff16100843123480a0970 #jenkins-foo #jenkins-bar]```", + }) { + Pullrequest.Comment comment = EasyMock.createNiceMock(Pullrequest.Comment.class); + EasyMock.expect(comment.getContent()).andReturn(commentContent).anyTimes(); + EasyMock.expect(comment.getId()).andReturn(new java.sql.Timestamp(Calendar.getInstance().getTime().getTime()).getNanos()).anyTimes(); + EasyMock.replay(comment); + comments.add(comment); + } + + String myBuildKey = "902f259e962ff16100843123480a0970"; + for(Pullrequest.Comment comment : comments) + assertTrue(repo.hasMyBuildTagInTTPComment(comment.getContent(), myBuildKey)); + } + + @Test + @WithoutJenkins + public void checkHashMyBuildTagFalse() { + BitbucketPullRequestsBuilder builder = EasyMock.createMock(BitbucketPullRequestsBuilder.class); + EasyMock.expect(builder.getTrigger()).andReturn(null).anyTimes(); + EasyMock.replay(builder); + + IMockBuilder repoBuilder = EasyMock.partialMockBuilder(BitbucketRepository.class); + repoBuilder.addMockedMethod("getMyBuildTag"); + BitbucketRepository repo = repoBuilder.createMock(); + EasyMock.expect(repo.getMyBuildTag(EasyMock.anyString())).andReturn("#jenkins-902f259e962ff16100843123480a0970").anyTimes(); + EasyMock.replay(repo); + + List comments = new LinkedList(); + for(String commentContent : new String[] { + "check", + "", + "Hello from mock", + "Jenkins: test this please", + "TTP build flag [bid: #jenkins]", + "TTP build flag [bid: #jenkins-foo]", + "TTP build flag [bid: #jenkins-foo #jenkins-bar]", + "TTP build flag ```[bid: #jenkins-foo #jenkins-bar]```", + }) { + Pullrequest.Comment comment = EasyMock.createNiceMock(Pullrequest.Comment.class); + EasyMock.expect(comment.getContent()).andReturn(commentContent).anyTimes(); + EasyMock.expect(comment.getId()).andReturn(new java.sql.Timestamp(Calendar.getInstance().getTime().getTime()).getNanos()).anyTimes(); + EasyMock.replay(comment); + comments.add(comment); + } + + String myBuildKey = "902f259e962ff16100843123480a0970"; + for(Pullrequest.Comment comment : comments) + assertFalse(repo.hasMyBuildTagInTTPComment(comment.getContent(), myBuildKey)); + } + + //@Test + @WithoutJenkins + public void ttpCommentTest() { + ApiClient client = EasyMock.createNiceMock(ApiClient.class); + Collection> prs = new LinkedList>(); + + prs.add(Arrays.asList(new Pullrequest[] { + new Pullrequest() + })); + + for(List 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 targetPRs = repo.getTargetPullRequests(); + } } -- cgit v1.2.3