Skip to content

Commit

Permalink
#395 - Make 'Closing keywords' referencing the issue mandatory in Git…
Browse files Browse the repository at this point in the history
…Hub pull requests
  • Loading branch information
Nico Peeters committed Jun 26, 2018
1 parent d0e6854 commit f2f96ba
Show file tree
Hide file tree
Showing 10 changed files with 392 additions and 84 deletions.
15 changes: 15 additions & 0 deletions github/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,21 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public class GithubResult {
private String number;
private String title;
private String state;
@JsonProperty("body_html")
private String body;
@JsonProperty("body_html")
private String bodyHtml;
private GithubUser user;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.fundrequest.platform.github.scraper;

import io.fundrequest.common.infrastructure.JsoupSpringWrapper;
import io.fundrequest.platform.github.scraper.model.GithubId;
import io.fundrequest.platform.github.scraper.model.GithubIssue;
import org.jsoup.nodes.Document;
import org.springframework.stereotype.Component;
Expand Down Expand Up @@ -29,7 +30,7 @@ public GithubIssue fetchGithubIssue(final String owner, final String repo, final
}
return GithubIssue.builder()
.number(number)
.solver(solverResolver.resolve(document, owner, repo))
.solver(solverResolver.resolve(document, GithubId.builder().owner(owner).repo(repo).number(number).build()).orElse(null))
.status(statusResolver.resolve(document))
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,75 @@

import io.fundrequest.platform.github.GithubGateway;
import io.fundrequest.platform.github.parser.GithubResult;
import io.fundrequest.platform.github.scraper.model.GithubId;
import org.apache.commons.lang.StringUtils;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;

@Component
public class GithubSolverResolver {

private static final List<String> CLOSING_KEYWORDS = Arrays.asList("close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved");

private final GithubGateway githubGateway;

public GithubSolverResolver(final GithubGateway githubGateway) {
this.githubGateway = githubGateway;
}

public String resolve(final Document document, final String owner, final String repo) {
public Optional<String> resolve(final Document document, final GithubId issueGithubId) {
return document.select(".discussion-item")
.stream()
.filter(this::isPullRequest)
.filter(this::isMerged)
.map(this::resolvePullRequestNumber)
.map(pullRequestNumber -> fetchAuthorFromPullRequest(pullRequestNumber, owner, repo))
.map(this::resolvePullRequestGithubId)
.map(pullRequestGithubId -> fetchAuthorFromPullRequest(pullRequestGithubId, issueGithubId))
.filter(Optional::isPresent)
.map(Optional::get)
.filter(StringUtils::isNotEmpty)
.findFirst()
.orElse(null);
.findFirst();
}

private String resolvePullRequestNumber(final Element discussionItem) {
final String pullRequestNumber;
private GithubId resolvePullRequestGithubId(final Element discussionItem) {
if (isPullRequestInSingleDiscussionItem(discussionItem)) {
pullRequestNumber = getPullRequestNumberFromSingleDiscussionItem(discussionItem);
return getPullRequestGithubIdFromSingleDiscussionItem(discussionItem);
} else {
pullRequestNumber = getPullRequestNumberFromInlineDiscussionItem(discussionItem);
return getPullRequestGithubIdFromInlineDiscussionItem(discussionItem);
}
return pullRequestNumber.replace("#", "");
}

private String getPullRequestNumberFromSingleDiscussionItem(final Element discussionItem) {
return discussionItem.select(".discussion-item [id^=ref-pullrequest-] ~ .discussion-item-ref-title span.issue-num").text();
private GithubId getPullRequestGithubIdFromSingleDiscussionItem(final Element discussionItem) {
return GithubId.fromString(discussionItem.select(".discussion-item [id^=ref-pullrequest-] ~ .discussion-item-ref-title a").attr("href"))
.orElseThrow(() -> new RuntimeException("No pullrequest identifier is found"));
}

private GithubId getPullRequestGithubIdFromInlineDiscussionItem(final Element discussionItem) {
return GithubId.fromString(discussionItem.select(".discussion-item [id^=ref-pullrequest-] a").attr("href"))
.orElseThrow(() -> new RuntimeException("No pullrequest identifier is found"));
}

private String getPullRequestNumberFromInlineDiscussionItem(final Element discussionItem) {
return discussionItem.select(".discussion-item [id^=ref-pullrequest-] span.issue-num").text();
private Optional<String> fetchAuthorFromPullRequest(final GithubId pullRequestGithubId, final GithubId issueGithubId) {
final GithubResult pullRequest = githubGateway.getPullrequest(pullRequestGithubId.getOwner(), pullRequestGithubId.getRepo(), pullRequestGithubId.getNumber());
if (pullRequest != null && pullRequestFixesIssue(pullRequest, issueGithubId)) {
return Optional.of(pullRequest.getUser().getLogin());
}
return Optional.empty();
}

private String fetchAuthorFromPullRequest(final String pullRequestNumber, final String owner, final String repo) {
final GithubResult pullRequest = githubGateway.getPullrequest(owner, repo, pullRequestNumber);
return pullRequest.getUser().getLogin();
private boolean pullRequestFixesIssue(final GithubResult pullRequest, final GithubId issueGithubId) {
final String pullRequestBody = pullRequest.getBody();

return pullRequestBody != null && CLOSING_KEYWORDS.stream()
.anyMatch(keyword -> Pattern.compile("\\b" + keyword.toLowerCase() + "\\b:?\\s*#" + issueGithubId.getNumber())
.matcher(pullRequestBody.toLowerCase())
.find());
}

private boolean isPullRequestInSingleDiscussionItem(final Element discussionItem) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.fundrequest.platform.github.scraper.model;

import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;

import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Getter
@Builder
@EqualsAndHashCode
public class GithubId {

private final String owner;
private final String repo;
private final String number;

public static Optional<GithubId> fromString(final String githubIdAsString) {
final Pattern pattern = Pattern.compile("^.*?/?(?<owner>.+)/(?<repo>.+)/.+/(?<number>\\d+)$");
final Matcher matcher = pattern.matcher(githubIdAsString);
if (matcher.matches()) {
return Optional.of(GithubId.builder()
.owner(matcher.group("owner"))
.repo(matcher.group("repo"))
.number(matcher.group("number"))
.build());
}
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package io.fundrequest.platform.github.scraper;

import io.fundrequest.platform.github.scraper.model.GithubId;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.util.ArrayList;
import java.util.List;

import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

Expand Down Expand Up @@ -41,7 +43,7 @@ public static class DiscussionItemBuilder {
private final Element element;

private DiscussionItemBuilder() {
this.element = mock(Element.class);
this.element = mock(Element.class, RETURNS_DEEP_STUBS);
}

public static DiscussionItemBuilder builder() {
Expand Down Expand Up @@ -69,19 +71,17 @@ public DiscussionItemBuilder withAuthor(final String author) {
return this;
}

public DiscussionItemBuilder withIssueNum(final int issueNum, final boolean isInlinePullRequest) {
public DiscussionItemBuilder withPullrequestReference(final GithubId githubId, final boolean isInlinePullRequest) {
final Elements pullRequestElements = mock(Elements.class);
final Elements pullRequestIssueNumElements = mock(Elements.class);

when(pullRequestElements.isEmpty()).thenReturn(!isInlinePullRequest);
when(element.select(".discussion-item .discussion-item-rollup-ref [id^=ref-pullrequest-]")).thenReturn(pullRequestElements);

final String githubIssueId = String.format("/%s/%s/pulls/%s", githubId.getOwner(), githubId.getRepo(), githubId.getNumber());
if (isInlinePullRequest) {
when(pullRequestIssueNumElements.text()).thenReturn("#" + String.valueOf(issueNum));
when(element.select(".discussion-item [id^=ref-pullrequest-] span.issue-num")).thenReturn(pullRequestIssueNumElements);
when(element.select(".discussion-item [id^=ref-pullrequest-] a").attr("href")).thenReturn(githubIssueId);
} else {
when(pullRequestIssueNumElements.text()).thenReturn("#" + String.valueOf(issueNum));
when(element.select(".discussion-item [id^=ref-pullrequest-] ~ .discussion-item-ref-title span.issue-num")).thenReturn(pullRequestIssueNumElements);
when(element.select(".discussion-item [id^=ref-pullrequest-] ~ .discussion-item-ref-title a").attr("href")).thenReturn(githubIssueId);
}
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ public void fetch_parsesCorrectSolver_whenReferencerInIssueIsNotOwnerPullRequest

assertThat(githubIssue.getNumber()).isEqualTo("48");
assertThat(githubIssue.getSolver()).isEqualTo("pauliax");
//TODO Uncomment when issue https://github.com/FundRequest/contracts/issues/48 is closed.
// assertThat(githubIssue.getStatus()).isEqualTo("Closed");
assertThat(githubIssue.getStatus()).isEqualTo("Closed");
}

@Test
Expand All @@ -70,4 +69,43 @@ public void fetch_whenNoSolver() {
assertThat(githubIssue.getSolver()).isNull();
assertThat(githubIssue.getStatus()).isEqualTo("Closed");
}

@Test
public void fetch_issueRandomlyReferencedInPullRequestFromSameRepo() {
final String owner = "aragon";
final String repo = "aragonOS";
final String number = "280";

final GithubIssue githubIssue = scraper.fetchGithubIssue(owner, repo, number);

assertThat(githubIssue.getNumber()).isEqualTo("280");
assertThat(githubIssue.getSolver()).isNull();
assertThat(githubIssue.getStatus()).isEqualTo("Open");
}

@Test
public void fetch_issueRandomlyReferencedInPullRequestFromOtherRepo() {
final String owner = "trufflesuite";
final String repo = "truffle";
final String number = "501";

final GithubIssue githubIssue = scraper.fetchGithubIssue(owner, repo, number);

assertThat(githubIssue.getNumber()).isEqualTo("501");
assertThat(githubIssue.getSolver()).isNull();
assertThat(githubIssue.getStatus()).isEqualTo("Open");
}

@Test
public void fetch_() {
final String owner = "FundRequest";
final String repo = "contracts";
final String number = "50";

final GithubIssue githubIssue = scraper.fetchGithubIssue(owner, repo, number);

assertThat(githubIssue.getNumber()).isEqualTo("50");
assertThat(githubIssue.getSolver()).isEqualTo("thomasvds");
assertThat(githubIssue.getStatus()).isEqualTo("Closed");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@


import io.fundrequest.common.infrastructure.JsoupSpringWrapper;
import io.fundrequest.platform.github.scraper.model.GithubId;
import io.fundrequest.platform.github.scraper.model.GithubIssue;
import org.jsoup.nodes.Document;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
Expand Down Expand Up @@ -39,7 +41,7 @@ public void fetchGithubIssue() throws IOException {
final Document document = mock(Document.class);

when(jsoup.connect("https://github.com/" + owner + "/" + repo + "/issues/" + number).get()).thenReturn(document);
when(solverParser.resolve(document, owner, repo)).thenReturn(expectedSolver);
when(solverParser.resolve(document, GithubId.builder().owner(owner).repo(repo).number(number).build())).thenReturn(Optional.of(expectedSolver));
when(statusParser.resolve(document)).thenReturn(expectedStatus);

final GithubIssue returnedIssue = scraper.fetchGithubIssue(owner, repo, number);
Expand Down
Loading

0 comments on commit f2f96ba

Please sign in to comment.