Add support for monitoring issues that are waiting for feedback
This commit is contained in:
@@ -16,6 +16,8 @@
|
||||
|
||||
package io.spring.issuebot;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@@ -54,6 +56,11 @@ public class GitHubProperties {
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* The names of the repository's collaborators.
|
||||
*/
|
||||
private List<String> collaborators;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
import io.spring.issuebot.github.GitHubOperations;
|
||||
import io.spring.issuebot.github.GitHubTemplate;
|
||||
@@ -33,6 +34,7 @@ import io.spring.issuebot.github.RegexLinkParser;
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
@EnableConfigurationProperties(GitHubProperties.class)
|
||||
public class IssueBotApplication {
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.spring.issuebot.feedback;
|
||||
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import io.spring.issuebot.GitHubProperties;
|
||||
import io.spring.issuebot.github.GitHubOperations;
|
||||
|
||||
/**
|
||||
* Central configuration for the beans involved in managing issues that are waiting for
|
||||
* feedback.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(FeedbackProperties.class)
|
||||
class FeedbackConfiguration {
|
||||
|
||||
@Bean
|
||||
FeedbackIssueListener feedbackIssueListener(GitHubOperations gitHub,
|
||||
GitHubProperties githubProperties, FeedbackProperties feedbackProperties) {
|
||||
return new FeedbackIssueListener(gitHub, feedbackProperties.getRequiredLabel(),
|
||||
githubProperties.getRepository().getCollaborators(),
|
||||
new StandardFeedbackListener(gitHub,
|
||||
feedbackProperties.getProvidedLabel(),
|
||||
feedbackProperties.getRequiredLabel(),
|
||||
feedbackProperties.getReminderComment(),
|
||||
feedbackProperties.getCloseComment()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.spring.issuebot.feedback;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import io.spring.issuebot.IssueListener;
|
||||
import io.spring.issuebot.github.Comment;
|
||||
import io.spring.issuebot.github.Event;
|
||||
import io.spring.issuebot.github.GitHubOperations;
|
||||
import io.spring.issuebot.github.Issue;
|
||||
import io.spring.issuebot.github.Label;
|
||||
import io.spring.issuebot.github.Page;
|
||||
|
||||
/**
|
||||
* An {@link IssueListener} that processes issues that are waiting for feedback.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
final class FeedbackIssueListener implements IssueListener {
|
||||
|
||||
private final GitHubOperations gitHub;
|
||||
|
||||
private final String labelName;
|
||||
|
||||
private final List<String> collaborators;
|
||||
|
||||
private final FeedbackListener feedbackListener;
|
||||
|
||||
FeedbackIssueListener(GitHubOperations gitHub, String labelName,
|
||||
List<String> collaborators, FeedbackListener feedbackListener) {
|
||||
this.gitHub = gitHub;
|
||||
this.labelName = labelName;
|
||||
this.collaborators = collaborators;
|
||||
this.feedbackListener = feedbackListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpenIssue(Issue issue) {
|
||||
if (waitingForFeedback(issue)) {
|
||||
OffsetDateTime waitingSince = getWaitingSince(issue);
|
||||
if (waitingSince != null) {
|
||||
processWaitingIssue(issue, waitingSince);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processWaitingIssue(Issue issue, OffsetDateTime waitingSince) {
|
||||
if (commentedSince(waitingSince, issue)) {
|
||||
this.feedbackListener.feedbackProvided(issue);
|
||||
}
|
||||
else {
|
||||
this.feedbackListener.feedbackRequired(issue, waitingSince);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean waitingForFeedback(Issue issue) {
|
||||
return issue.getPullRequest() == null && labelledAsWaitingForFeedback(issue);
|
||||
}
|
||||
|
||||
private boolean labelledAsWaitingForFeedback(Issue issue) {
|
||||
for (Label label : issue.getLabels()) {
|
||||
if (this.labelName.equals(label.getName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private OffsetDateTime getWaitingSince(Issue issue) {
|
||||
OffsetDateTime createdAt = null;
|
||||
Page<Event> page = this.gitHub.getEvents(issue);
|
||||
while (page != null) {
|
||||
for (Event event : page.getContent()) {
|
||||
if (Event.Type.LABELED.equals(event.getType())
|
||||
&& this.labelName.equals(event.getLabel().getName())) {
|
||||
createdAt = event.getCreationTime();
|
||||
}
|
||||
}
|
||||
page = page.next();
|
||||
}
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
private boolean commentedSince(OffsetDateTime waitingForFeedbackSince, Issue issue) {
|
||||
Page<Comment> page = this.gitHub.getComments(issue);
|
||||
while (page != null) {
|
||||
for (Comment comment : page.getContent()) {
|
||||
if (!this.collaborators.contains(comment.getUser().getLogin())
|
||||
&& comment.getCreationTime().isAfter(waitingForFeedbackSince)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
page = page.next();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.spring.issuebot.feedback;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import io.spring.issuebot.github.Issue;
|
||||
|
||||
/**
|
||||
* A {@code FeedbackListener} is notified when feedback has been provided or is still
|
||||
* required for an {@link Issue}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public interface FeedbackListener {
|
||||
|
||||
/**
|
||||
* Notification that feedback has been provided for the given {@code issue}.
|
||||
*
|
||||
* @param issue the issue
|
||||
*/
|
||||
void feedbackProvided(Issue issue);
|
||||
|
||||
/**
|
||||
* Notification that feedback is still required for the given {@code issue} having
|
||||
* been requested at the given {@code requestTime}.
|
||||
*
|
||||
* @param issue the issue
|
||||
* @param requestTime the time when feedback was requested
|
||||
*/
|
||||
void feedbackRequired(Issue issue, OffsetDateTime requestTime);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.spring.issuebot.feedback;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
|
||||
/**
|
||||
* {@link EnableConfigurationProperties Configuration properties} for configuring the
|
||||
* monitoring of issues that require user feedback.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ConfigurationProperties(prefix = "issuebot.feedback")
|
||||
final class FeedbackProperties {
|
||||
|
||||
/**
|
||||
* Name of the label that is applied when feedback is required.
|
||||
*/
|
||||
private String requiredLabel;
|
||||
|
||||
/**
|
||||
* Name of the label that is applied when feedback has been provided.
|
||||
*/
|
||||
private String providedLabel;
|
||||
|
||||
/**
|
||||
* The text of the comment that is added as a reminder that feedback is required.
|
||||
*/
|
||||
private String reminderComment;
|
||||
|
||||
/**
|
||||
* The text of the comment that is added when an issue is clsed as feedback has not
|
||||
* been provided.
|
||||
*/
|
||||
private String closeComment;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.spring.issuebot.feedback;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import io.spring.issuebot.github.GitHubOperations;
|
||||
import io.spring.issuebot.github.Issue;
|
||||
|
||||
/**
|
||||
* Standard implementation of {@link FeedbackListener}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
final class StandardFeedbackListener implements FeedbackListener {
|
||||
|
||||
private final GitHubOperations gitHub;
|
||||
|
||||
private final String providedLabel;
|
||||
|
||||
private final String requiredLabel;
|
||||
|
||||
private final String reminderComment;
|
||||
|
||||
private final String closeComment;
|
||||
|
||||
StandardFeedbackListener(GitHubOperations gitHub, String providedLabel,
|
||||
String requiredLabel, String reminderComment, String closeComment) {
|
||||
this.gitHub = gitHub;
|
||||
this.providedLabel = providedLabel;
|
||||
this.requiredLabel = requiredLabel;
|
||||
this.reminderComment = reminderComment;
|
||||
this.closeComment = closeComment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void feedbackProvided(Issue issue) {
|
||||
this.gitHub.addLabel(issue, this.providedLabel);
|
||||
this.gitHub.removeLabel(issue, this.requiredLabel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void feedbackRequired(Issue issue, OffsetDateTime requestTime) {
|
||||
OffsetDateTime now = OffsetDateTime.now();
|
||||
if (requestTime.plusDays(14).isBefore(now)) {
|
||||
close(issue);
|
||||
}
|
||||
else if (requestTime.plusDays(7).isBefore(now)) {
|
||||
remind(issue);
|
||||
}
|
||||
}
|
||||
|
||||
private void close(Issue issue) {
|
||||
this.gitHub.addComment(issue, this.closeComment);
|
||||
this.gitHub.close(issue);
|
||||
}
|
||||
|
||||
private void remind(Issue issue) {
|
||||
this.gitHub.addComment(issue, this.reminderComment);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package io.spring.issuebot.github;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Getter;
|
||||
@@ -30,14 +32,20 @@ public final class Comment {
|
||||
|
||||
private final User user;
|
||||
|
||||
private final OffsetDateTime creationTime;
|
||||
|
||||
/**
|
||||
* Creates a new comment that was authored by the given {@code user}.
|
||||
* Creates a new comment that was authored by the given {@code user} at the given
|
||||
* {@code creationTime}.
|
||||
*
|
||||
* @param user the user
|
||||
* @param creationTime the creation time
|
||||
*/
|
||||
@JsonCreator
|
||||
public Comment(@JsonProperty("user") User user) {
|
||||
public Comment(@JsonProperty("user") User user,
|
||||
@JsonProperty("created_at") OffsetDateTime creationTime) {
|
||||
this.user = user;
|
||||
this.creationTime = creationTime;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
164
src/main/java/io/spring/issuebot/github/Event.java
Normal file
164
src/main/java/io/spring/issuebot/github/Event.java
Normal file
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.spring.issuebot.github;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* An event that has been performed on an {@link Issue}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@Getter
|
||||
public class Event {
|
||||
|
||||
private final Type type;
|
||||
|
||||
private final OffsetDateTime creationTime;
|
||||
|
||||
private final Label label;
|
||||
|
||||
/**
|
||||
* Creates a new {@code Event}.
|
||||
*
|
||||
* @param type the type of the event
|
||||
* @param creationTime the timestamp of when the event was created
|
||||
* @param label the label associated with the event
|
||||
*/
|
||||
@JsonCreator
|
||||
public Event(@JsonProperty("event") String type,
|
||||
@JsonProperty("created_at") OffsetDateTime creationTime,
|
||||
@JsonProperty("label") Label label) {
|
||||
this.type = Type.valueFrom(type);
|
||||
this.creationTime = creationTime;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of an {@link Event}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public enum Type {
|
||||
|
||||
/**
|
||||
* The issue was closed by the actor.
|
||||
*/
|
||||
CLOSED("closed"),
|
||||
|
||||
/**
|
||||
* The issue was reopened by the actor.
|
||||
*/
|
||||
REOPENED("reopened"),
|
||||
|
||||
/**
|
||||
* The actor subscribed to receive notifications for an issue.
|
||||
*/
|
||||
SUBSCRIBED("subscribed"),
|
||||
|
||||
/**
|
||||
* The issue was merged by the actor.
|
||||
*/
|
||||
MERGED("merged"),
|
||||
|
||||
/**
|
||||
* The issue was referenced from a commit message.
|
||||
*/
|
||||
REFERENCED("referenced"),
|
||||
|
||||
/**
|
||||
* The actor was {@code @mentioned} in an issue body.
|
||||
*/
|
||||
MENTIONED("mentioned"),
|
||||
|
||||
/**
|
||||
* The issue was assigned to the actor.
|
||||
*/
|
||||
ASSIGNED("assigned"),
|
||||
|
||||
/**
|
||||
* The actor was unassigned from the issue.
|
||||
*/
|
||||
UNASSIGNED("unassigned"),
|
||||
|
||||
/**
|
||||
* A label was added to the issue.
|
||||
*/
|
||||
LABELED("labeled"),
|
||||
|
||||
/**
|
||||
* A label was removed from the issue.
|
||||
*/
|
||||
UNLABELED("unlabeled"),
|
||||
|
||||
/**
|
||||
* The issue was added to a milestone.
|
||||
*/
|
||||
MILESTONED("milestoned"),
|
||||
|
||||
/**
|
||||
* The issue was removed from a milestone.
|
||||
*/
|
||||
DEMILESTONED("demilestoned"),
|
||||
|
||||
/**
|
||||
* The issue title was changed.
|
||||
*/
|
||||
RENAMED("renamed"),
|
||||
|
||||
/**
|
||||
* The issue was locked by the actor.
|
||||
*/
|
||||
LOCKED("locked"),
|
||||
|
||||
/**
|
||||
* The issue was unlocked by the actor.
|
||||
*/
|
||||
UNLOCKED("unlocked"),
|
||||
|
||||
/**
|
||||
* The pull request's branch was deleted.
|
||||
*/
|
||||
HEAD_REF_DELETED("head_ref_deleted"),
|
||||
|
||||
/**
|
||||
* The pull request's branch was restored.
|
||||
*/
|
||||
HEAD_REF_RESTORED("head_ref_restored");
|
||||
|
||||
private String type;
|
||||
|
||||
Type(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
static Type valueFrom(String type) {
|
||||
for (Type value : values()) {
|
||||
if (type.equals(value.type)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
"'" + type + "' is not a valid event type");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -45,9 +45,43 @@ public interface GitHubOperations {
|
||||
* Adds the given {@code label} to the given {@code issue}.
|
||||
*
|
||||
* @param issue the issue
|
||||
* @param label the label;
|
||||
* @return the updated issue
|
||||
* @param label the label
|
||||
* @return the modified issue
|
||||
*/
|
||||
Issue addLabel(Issue issue, String label);
|
||||
|
||||
/**
|
||||
* Removes the given {@code label} from the given {@code issue}.
|
||||
*
|
||||
* @param issue the issue
|
||||
* @param label the label
|
||||
* @return the modified issue
|
||||
*/
|
||||
Issue removeLabel(Issue issue, String label);
|
||||
|
||||
/**
|
||||
* Adds the given {@code comment} to the given {@code issue}.
|
||||
*
|
||||
* @param issue the issue
|
||||
* @param comment the comment
|
||||
* @return the added comment
|
||||
*/
|
||||
Comment addComment(Issue issue, String comment);
|
||||
|
||||
/**
|
||||
* Closes the given {@code issue}.
|
||||
*
|
||||
* @param issue the issue
|
||||
* @return the modified issue
|
||||
*/
|
||||
Issue close(Issue issue);
|
||||
|
||||
/**
|
||||
* Returns the events that have occurred on the given {@code issue}.
|
||||
*
|
||||
* @param issue the issue
|
||||
* @return the events
|
||||
*/
|
||||
Page<Event> getEvents(Issue issue);
|
||||
|
||||
}
|
||||
|
||||
@@ -73,10 +73,6 @@ public class GitHubTemplate implements GitHubOperations {
|
||||
this.linkParser = linkParser;
|
||||
}
|
||||
|
||||
RestOperations getRestOperations() {
|
||||
return this.rest;
|
||||
}
|
||||
|
||||
static RestTemplate createDefaultRestTemplate(String username, String password) {
|
||||
RestTemplate rest = new RestTemplate();
|
||||
rest.setErrorHandler(new DefaultResponseErrorHandler() {
|
||||
@@ -103,48 +99,83 @@ public class GitHubTemplate implements GitHubOperations {
|
||||
public Page<Issue> getIssues(String organization, String repository) {
|
||||
String url = "https://api.github.com/repos/" + organization + "/" + repository
|
||||
+ "/issues";
|
||||
return getIssues(url);
|
||||
return getPage(url, Issue[].class);
|
||||
}
|
||||
|
||||
private Page<Issue> getIssues(String url) {
|
||||
@Override
|
||||
public Page<Comment> getComments(Issue issue) {
|
||||
return getPage(issue.getCommentsUrl(), Comment[].class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<Event> getEvents(Issue issue) {
|
||||
return getPage(issue.getEventsUrl(), Event[].class);
|
||||
}
|
||||
|
||||
private <T> Page<T> getPage(String url, Class<T[]> type) {
|
||||
if (!StringUtils.hasText(url)) {
|
||||
return null;
|
||||
}
|
||||
ResponseEntity<Issue[]> issues = this.rest.getForEntity(url, Issue[].class);
|
||||
return new StandardPage<Issue>(Arrays.asList(issues.getBody()),
|
||||
() -> getIssues(getNextUrl(issues)));
|
||||
ResponseEntity<T[]> contents = this.rest.getForEntity(url, type);
|
||||
return new StandardPage<T>(Arrays.asList(contents.getBody()),
|
||||
() -> getPage(getNextUrl(contents), type));
|
||||
}
|
||||
|
||||
private String getNextUrl(ResponseEntity<?> response) {
|
||||
return this.linkParser.parse(response.getHeaders().getFirst("Link")).get("next");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<Comment> getComments(Issue issue) {
|
||||
return getComments(issue.getCommentsUrl());
|
||||
}
|
||||
|
||||
private Page<Comment> getComments(String url) {
|
||||
if (!StringUtils.hasText(url)) {
|
||||
return null;
|
||||
}
|
||||
ResponseEntity<Comment[]> comments = this.rest.getForEntity(url, Comment[].class);
|
||||
return new StandardPage<Comment>(Arrays.asList(comments.getBody()),
|
||||
() -> getComments(getNextUrl(comments)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Issue addLabel(Issue issue, String labelName) {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("labels", Arrays.asList(labelName));
|
||||
ResponseEntity<Issue> exchange = this.rest.exchange(
|
||||
ResponseEntity<Label[]> response = this.rest.exchange(
|
||||
new RequestEntity<>(body, HttpMethod.POST, URI.create(issue.getUrl())),
|
||||
Issue.class);
|
||||
if (exchange.getStatusCode() != HttpStatus.OK) {
|
||||
Label[].class);
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
log.warn("Failed to add label to issue. Response status: "
|
||||
+ exchange.getStatusCode());
|
||||
+ response.getStatusCode());
|
||||
}
|
||||
return exchange.getBody();
|
||||
return new Issue(issue.getUrl(), issue.getCommentsUrl(), issue.getEventsUrl(),
|
||||
issue.getLabelsUrl(), issue.getUser(), Arrays.asList(response.getBody()),
|
||||
issue.getMilestone(), issue.getPullRequest());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Issue removeLabel(Issue issue, String labelName) {
|
||||
ResponseEntity<Label[]> response = this.rest.exchange(
|
||||
new RequestEntity<Void>(HttpMethod.DELETE, URI.create(
|
||||
issue.getLabelsUrl().replace("{/name}", "/" + labelName))),
|
||||
Label[].class);
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
log.warn("Failed to remove label from issue. Response status: "
|
||||
+ response.getStatusCode());
|
||||
}
|
||||
return new Issue(issue.getUrl(), issue.getCommentsUrl(), issue.getEventsUrl(),
|
||||
issue.getLabelsUrl(), issue.getUser(), Arrays.asList(response.getBody()),
|
||||
issue.getMilestone(), issue.getPullRequest());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comment addComment(Issue issue, String comment) {
|
||||
Map<String, String> body = new HashMap<>();
|
||||
body.put("body", comment);
|
||||
return this.rest.postForEntity(issue.getCommentsUrl(), body, Comment.class)
|
||||
.getBody();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Issue close(Issue issue) {
|
||||
Map<String, String> body = new HashMap<>();
|
||||
body.put("state", "closed");
|
||||
ResponseEntity<Issue> response = this.rest.exchange(
|
||||
new RequestEntity<>(body, HttpMethod.PATCH, URI.create(issue.getUrl())),
|
||||
Issue.class);
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
log.warn("Failed to close issue. Response status: "
|
||||
+ response.getStatusCode());
|
||||
}
|
||||
return response.getBody();
|
||||
}
|
||||
|
||||
private static class BasicAuthorizationInterceptor
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
@@ -27,38 +28,65 @@ import lombok.Getter;
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@Getter
|
||||
public class Issue {
|
||||
|
||||
@Getter(AccessLevel.PACKAGE)
|
||||
private final String url;
|
||||
|
||||
@Getter(AccessLevel.PACKAGE)
|
||||
private final String commentsUrl;
|
||||
|
||||
@Getter(AccessLevel.PACKAGE)
|
||||
private final String eventsUrl;
|
||||
|
||||
@Getter(AccessLevel.PACKAGE)
|
||||
private final String labelsUrl;
|
||||
|
||||
@Getter
|
||||
private final User user;
|
||||
|
||||
private final List<Label> labels;
|
||||
@Getter
|
||||
private List<Label> labels;
|
||||
|
||||
@Getter
|
||||
private final Milestone milestone;
|
||||
|
||||
@Getter
|
||||
private final PullRequest pullRequest;
|
||||
|
||||
/**
|
||||
* Creates a new {@code Issue}.
|
||||
*
|
||||
* @param url the url of the issue in the GitHub API
|
||||
* @param commentsUrl the url of the comments on the issue in the GitHub API
|
||||
* @param eventsUrl the url of the events on the issue in the GitHub API
|
||||
* @param labelsUrl the url of the labels on the issue in the GitHub API
|
||||
* @param user the user that created the issue
|
||||
* @param labels the labels applied to the issue
|
||||
* @param milestone the milestone applied to the issue
|
||||
* @param pullRequest details of the pull request (if this issue is a pull request)
|
||||
*/
|
||||
@JsonCreator
|
||||
public Issue(@JsonProperty("url") String url,
|
||||
@JsonProperty("comments_url") String commentsUrl,
|
||||
@JsonProperty("user") User user, @JsonProperty("labels") List<Label> labels,
|
||||
@JsonProperty("milestone") Milestone milestone) {
|
||||
@JsonProperty("events_url") String eventsUrl,
|
||||
@JsonProperty("labels_url") String labelsUrl, @JsonProperty("user") User user,
|
||||
@JsonProperty("labels") List<Label> labels,
|
||||
@JsonProperty("milestone") Milestone milestone,
|
||||
@JsonProperty("pull_request") PullRequest pullRequest) {
|
||||
this.url = url;
|
||||
this.commentsUrl = commentsUrl;
|
||||
this.eventsUrl = eventsUrl;
|
||||
this.labelsUrl = labelsUrl;
|
||||
this.user = user;
|
||||
this.labels = labels;
|
||||
this.milestone = milestone;
|
||||
this.pullRequest = pullRequest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
42
src/main/java/io/spring/issuebot/github/PullRequest.java
Normal file
42
src/main/java/io/spring/issuebot/github/PullRequest.java
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.spring.issuebot.github;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Details of a GitHub pull request.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@Getter
|
||||
public class PullRequest {
|
||||
|
||||
private final String url;
|
||||
|
||||
/**
|
||||
* Creates a new {@code PullRequest} that has the given {@code url} in the GitHub API.
|
||||
* @param url the url
|
||||
*/
|
||||
@JsonCreator
|
||||
public PullRequest(@JsonProperty("url") String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -40,4 +40,9 @@ public class User {
|
||||
this.login = login;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.login;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ final class CommentedByCollaboratorTriageFilter implements TriageFilter {
|
||||
while (page != null) {
|
||||
for (Comment comment : page.getContent()) {
|
||||
if (this.collaborators.contains(comment.getUser().getLogin())) {
|
||||
log.debug("{} has been triaged. It was commented on by {}",
|
||||
issue.getUrl(), comment.getUser().getLogin());
|
||||
log.debug("{} has been triaged. It was commented on by {}", issue,
|
||||
comment.getUser());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ final class LabelledTriageFilter implements TriageFilter {
|
||||
@Override
|
||||
public boolean triaged(Issue issue) {
|
||||
if (issue.getLabels() != null && !issue.getLabels().isEmpty()) {
|
||||
log.debug("{} has been triaged. It has been labelled.", issue.getUrl());
|
||||
log.debug("{} has been triaged. It has been labelled.", issue);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -45,8 +45,7 @@ final class OpenedByCollaboratorTriageFilter implements TriageFilter {
|
||||
@Override
|
||||
public boolean triaged(Issue issue) {
|
||||
if (this.collaborators.contains(issue.getUser().getLogin())) {
|
||||
log.debug("{} has been triaged. It was opened by {}", issue.getUrl(),
|
||||
issue.getUser().getLogin());
|
||||
log.debug("{} has been triaged. It was opened by {}", issue, issue.getUser());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -21,8 +21,8 @@ import java.util.Arrays;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
import io.spring.issuebot.GitHubProperties;
|
||||
import io.spring.issuebot.github.GitHubOperations;
|
||||
|
||||
/**
|
||||
@@ -31,20 +31,20 @@ import io.spring.issuebot.github.GitHubOperations;
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
@EnableConfigurationProperties(TriageProperties.class)
|
||||
class TriageConfiguration {
|
||||
|
||||
@Bean
|
||||
TriageIssueListener triageIssueListener(GitHubOperations gitHubOperations,
|
||||
TriageProperties triageProperties) {
|
||||
TriageProperties triageProperties, GitHubProperties gitHubProperties) {
|
||||
return new TriageIssueListener(
|
||||
Arrays.asList(
|
||||
new OpenedByCollaboratorTriageFilter(
|
||||
triageProperties.getCollaborators()),
|
||||
gitHubProperties.getRepository().getCollaborators()),
|
||||
new LabelledTriageFilter(), new MilestoneAppliedTriageFilter(),
|
||||
new CommentedByCollaboratorTriageFilter(
|
||||
triageProperties.getCollaborators(), gitHubOperations)),
|
||||
gitHubProperties.getRepository().getCollaborators(),
|
||||
gitHubOperations)),
|
||||
new LabelApplyingTriageListener(gitHubOperations,
|
||||
triageProperties.getLabel()));
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
package io.spring.issuebot.triage;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@@ -34,11 +32,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
@ConfigurationProperties(prefix = "issuebot.triage")
|
||||
class TriageProperties {
|
||||
|
||||
/**
|
||||
* The names of the project collaborators whose issues do not require triage.
|
||||
*/
|
||||
private List<String> collaborators;
|
||||
|
||||
/**
|
||||
* The name of the label that should be applied to issues that are waiting for triage.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user