diff --git a/application-local.template b/application-local.template
index 33be287..13c1367 100644
--- a/application-local.template
+++ b/application-local.template
@@ -3,6 +3,7 @@ git.username=
git.author=
git.email=
git.password=
+github.api.url=https://api.github.com
# Maven
maven.mavenHome=
@@ -12,6 +13,11 @@ deployment.repository-prefix=
deployment.password=
deployment.api-key=
+# Jira
+jira.username=
+jira.password=
+jira.url=https://jira.spring.io
+
# GPG
deployment.gpg.keyname=
deployment.gpg.password=
diff --git a/pom.xml b/pom.xml
index bcf50df..65ea289 100644
--- a/pom.xml
+++ b/pom.xml
@@ -103,6 +103,19 @@
0.9.1
+
+ com.github.tomakehurst
+ wiremock
+ 1.58
+ test
+
+
+
+ com.google.guava
+ guava
+ 19.0
+
+
diff --git a/src/main/java/org/springframework/data/release/cli/IssueTrackerCommands.java b/src/main/java/org/springframework/data/release/cli/IssueTrackerCommands.java
index 6d4742a..6e19e5b 100644
--- a/src/main/java/org/springframework/data/release/cli/IssueTrackerCommands.java
+++ b/src/main/java/org/springframework/data/release/cli/IssueTrackerCommands.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2014 the original author or authors.
+ * Copyright 2014-2016 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.
@@ -16,14 +16,14 @@
package org.springframework.data.release.cli;
import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.core.env.Environment;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.jira.Changelog;
-import org.springframework.data.release.jira.Credentials;
import org.springframework.data.release.jira.IssueTracker;
import org.springframework.data.release.jira.JiraConnector;
+import org.springframework.data.release.jira.Tickets;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.TrainIteration;
@@ -36,13 +36,13 @@ import org.springframework.util.StringUtils;
/**
* @author Oliver Gierke
+ * @author Mark Paluch
*/
@CliComponent
public class IssueTrackerCommands implements CommandMarker {
private final PluginRegistry tracker;
private final JiraConnector jira;
- private final Credentials credentials;
/**
* @param tracker must not be {@literal null}.
@@ -50,20 +50,15 @@ public class IssueTrackerCommands implements CommandMarker {
* @param environment must not be {@literal null}.
*/
@Autowired
- public IssueTrackerCommands(PluginRegistry tracker, JiraConnector jira,
- Environment environment) {
-
- String username = environment.getProperty("jira.username", (String) null);
- String password = environment.getProperty("jira.password", (String) null);
+ public IssueTrackerCommands(PluginRegistry tracker, JiraConnector jira) {
this.tracker = tracker;
this.jira = jira;
- this.credentials = StringUtils.hasText(username) ? new Credentials(username, password) : null;
}
- @CliCommand("jira evict")
- public void jiraEvict() {
- jira.reset();
+ @CliCommand("tracker evict")
+ public void evict() {
+ StreamSupport.stream(tracker.spliterator(), false).forEach(IssueTracker::reset);
}
@CliCommand(value = "jira tickets")
@@ -71,11 +66,52 @@ public class IssueTrackerCommands implements CommandMarker {
@CliOption(key = "for-current-user", specifiedDefaultValue = "true",
unspecifiedDefaultValue = "false") boolean forCurrentUser) {
- if (forCurrentUser && credentials == null) {
- return "No authentication specified! Use 'jira authenticate' first!";
- }
+ Tickets tickets = StreamSupport.stream(tracker.spliterator(), false). //
+ flatMap(issueTracker -> issueTracker. //
+ getTicketsFor(iteration, forCurrentUser).stream())
+ . //
+ collect(Tickets.toTicketsCollector());
+ return tickets.toString();
+ }
- return jira.getTicketsFor(iteration, forCurrentUser ? credentials : null).toString();
+ @CliCommand(value = "tracker releasetickets")
+ public String releaseTickets(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
+
+ Tickets tickets = StreamSupport.stream(tracker.spliterator(), false). //
+ flatMap(issueTracker -> issueTracker.getTicketsFor(iteration).stream()). //
+ collect(Tickets.toTicketsCollector());
+ return tickets.getReleaseTickets(iteration).toString();
+ }
+
+ @CliCommand(value = "jira self-assign releasetickets")
+ public String jiraSelfAssignReleaseTickets(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
+
+ Tickets releaseTickets = jira.getTicketsFor(iteration).getReleaseTickets(iteration);
+ releaseTickets.forEach(ticket -> jira.assignTicketToMe(ticket));
+ return releaseTickets(iteration);
+ }
+
+ @CliCommand(value = "tracker create releaseversions")
+ public void jiraCreateReleaseVersions(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
+ iteration.forEach(this::createReleaseVersion);
+ }
+
+ @CliCommand(value = "tracker create releasetickets")
+ public String createReleaseTickets(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
+
+ iteration.forEach(this::createReleaseVersion);
+
+ evict();
+
+ return releaseTickets(iteration);
+ }
+
+ private void createReleaseVersion(ModuleIteration moduleIteration) {
+ getPluginFor(moduleIteration).createReleaseVersion(moduleIteration);
+ }
+
+ private IssueTracker getPluginFor(ModuleIteration moduleIteration) {
+ return tracker.getPluginFor(moduleIteration.getProject());
}
@CliCommand("tracker changelog")
diff --git a/src/main/java/org/springframework/data/release/git/GitProperties.java b/src/main/java/org/springframework/data/release/git/GitProperties.java
index 0b2244d..3a96fa2 100644
--- a/src/main/java/org/springframework/data/release/git/GitProperties.java
+++ b/src/main/java/org/springframework/data/release/git/GitProperties.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2015 the original author or authors.
+ * Copyright 2015-2016 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.
@@ -23,6 +23,7 @@ import javax.annotation.PostConstruct;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.data.release.utils.HttpBasicCredentials;
import org.springframework.stereotype.Component;
@@ -32,22 +33,26 @@ import org.springframework.util.Assert;
* Configurable properties for Git.
*
* @author Oliver Gierke
+ * @author Mark Paluch
*/
@Data
@Component
@ConfigurationProperties(prefix = "git")
public class GitProperties {
- private @Getter(AccessLevel.PRIVATE) String username, password;
- private String author, email;
+ private @Getter(AccessLevel.PRIVATE) String password;
+ private String username, author, email;
+
+ @Value("${github.api.url}") private String githubApiBaseUrl;
@PostConstruct
public void init() {
Assert.hasText(username, "No GitHub username (git.username) configured!");
- Assert.hasText(username, "No GitHub password (git.password) configured!");
- Assert.hasText(username, "No Git author (git.author) configured!");
- Assert.hasText(username, "No Git email (git.email) configured!");
+ Assert.hasText(password, "No GitHub password (git.password) configured!");
+ Assert.hasText(author, "No Git author (git.author) configured!");
+ Assert.hasText(email, "No Git email (git.email) configured!");
+ Assert.hasText(githubApiBaseUrl, "No GitHub API base url configured!");
}
/**
diff --git a/src/main/java/org/springframework/data/release/jira/CreatedJiraIssue.java b/src/main/java/org/springframework/data/release/jira/CreatedJiraIssue.java
new file mode 100644
index 0000000..85521a3
--- /dev/null
+++ b/src/main/java/org/springframework/data/release/jira/CreatedJiraIssue.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2016 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 org.springframework.data.release.jira;
+
+import lombok.Data;
+
+/**
+ * Value object for a created Jira issue.
+ *
+ * @author Mark Paluch
+ */
+@Data
+public class CreatedJiraIssue {
+
+ private String id;
+ private String key;
+
+}
diff --git a/src/main/java/org/springframework/data/release/jira/GitHubIssue.java b/src/main/java/org/springframework/data/release/jira/GitHubIssue.java
index 8afb420..6a9c211 100644
--- a/src/main/java/org/springframework/data/release/jira/GitHubIssue.java
+++ b/src/main/java/org/springframework/data/release/jira/GitHubIssue.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2014 the original author or authors.
+ * Copyright 2014-2016 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.
@@ -15,25 +15,77 @@
*/
package org.springframework.data.release.jira;
-import lombok.Data;
-
import org.springframework.data.release.model.ModuleIteration;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import lombok.Value;
+
/**
* @author Oliver Gierke
+ * @author Mark Paluch
*/
-@Data
+@Value
class GitHubIssue {
private String number;
private String title;
private String state;
+ private Object milestone;
+
+ @JsonCreator
+ public GitHubIssue(@JsonProperty("number") String number, @JsonProperty("title") String title,
+ @JsonProperty("state") String state, @JsonProperty("milestone") Object milestone) {
+ this.number = number;
+ this.title = title;
+ this.state = state;
+ this.milestone = milestone;
+ }
+
+ public GitHubIssue(String title, Milestone milestone) {
+
+ this.title = title;
+ this.milestone = milestone.getNumber();
+ this.number = null;
+ this.state = null;
+ }
public String getId() {
+
+ if (number == null) {
+ return null;
+ }
return "#".concat(number);
}
- public boolean isReleaseTicket(ModuleIteration module) {
- return title.contains("Release") && title.contains(module.getShortVersionString());
+ /**
+ * @author Oliver Gierke
+ * @author Mark Paluch
+ */
+ @Value
+ static class Milestone {
+
+ private final String title;
+ private final Long number;
+ private final String description;
+
+ @JsonCreator
+ public Milestone(@JsonProperty("title") String title, @JsonProperty("number") Long number,
+ @JsonProperty("description") String description) {
+ this.title = title;
+ this.number = number;
+ this.description = description;
+ }
+
+ public Milestone(String title, String description) {
+ this.number = null;
+ this.title = title;
+ this.description = description;
+ }
+
+ public boolean matchesIteration(ModuleIteration moduleIteration) {
+ return title.contains(moduleIteration.getShortVersionString());
+ }
}
}
diff --git a/src/main/java/org/springframework/data/release/jira/GitHubIssueTracker.java b/src/main/java/org/springframework/data/release/jira/GitHubIssueTracker.java
index 340fc59..2c085db 100644
--- a/src/main/java/org/springframework/data/release/jira/GitHubIssueTracker.java
+++ b/src/main/java/org/springframework/data/release/jira/GitHubIssueTracker.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2014 the original author or authors.
+ * Copyright 2014-2016 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.
@@ -15,8 +15,6 @@
*/
package org.springframework.data.release.jira;
-import lombok.RequiredArgsConstructor;
-
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
@@ -24,7 +22,7 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.stream.Collectors;
+import java.util.Optional;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
@@ -45,21 +43,28 @@ import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
+import org.springframework.util.Assert;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.util.UriTemplate;
+import lombok.RequiredArgsConstructor;
+
/**
* @author Oliver Gierke
+ * @author Mark Paluch
*/
@RequiredArgsConstructor
class GitHubIssueTracker implements IssueTracker {
- private static final String MILESTONE_URI = "https://api.github.com/repos/spring-projects/{repoName}/milestones?state={state}";
- private static final String ISSUES_BY_MILESTONE_URI_TEMPLATE = "https://api.github.com/repos/spring-projects/{repoName}/issues?milestone={id}&state=all";
- private static final String ISSUE_BY_ID_URI_TEMPLATE = "https://api.github.com/repos/spring-projects/{repoName}/issues/{id}";
+ private static final String MILESTONE_URI = "{githubBaseUrl}/repos/spring-projects/{repoName}/milestones?state={state}";
+ private static final String ISSUES_BY_MILESTONE_AND_ASSIGNEE_URI_TEMPLATE = "{githubBaseUrl}/repos/spring-projects/{repoName}/issues?milestone={id}&state=all&assignee={assignee}";
+ private static final String ISSUES_BY_MILESTONE_URI_TEMPLATE = "{githubBaseUrl}/repos/spring-projects/{repoName}/issues?milestone={id}&state=all";
+ private static final String MILESTONES_URI_TEMPLATE = "{githubBaseUrl}/repos/spring-projects/{repoName}/milestones";
+ private static final String ISSUE_BY_ID_URI_TEMPLATE = "{githubBaseUrl}/repos/spring-projects/{repoName}/issues/{id}";
+ private static final String ISSUES_URI_TEMPLATE = "{githubBaseUrl}/repos/spring-projects/{repoName}/issues";
- private static final ParameterizedTypeReference> MILESTONES_TYPE = new ParameterizedTypeReference>() {};
+ private static final ParameterizedTypeReference> MILESTONES_TYPE = new ParameterizedTypeReference>() {};
private static final ParameterizedTypeReference> ISSUES_TYPE = new ParameterizedTypeReference>() {};
private static final ParameterizedTypeReference ISSUE_TYPE = new ParameterizedTypeReference() {};
@@ -72,7 +77,7 @@ class GitHubIssueTracker implements IssueTracker {
* @see org.springframework.data.release.jira.JiraConnector#flushTickets()
*/
@Override
- @CacheEvict(value = "tickets", allEntries = true)
+ @CacheEvict(value = { "tickets", "release-tickets", "milestone" }, allEntries = true)
public void reset() {
}
@@ -84,20 +89,10 @@ class GitHubIssueTracker implements IssueTracker {
@Override
@Cacheable("release-tickets")
public Ticket getReleaseTicketFor(ModuleIteration module) {
-
- return getIssuesFor(module).stream().//
- filter(issue -> issue.isReleaseTicket(module)).//
- findFirst().//
- map(issue -> toTicket(issue)).//
- orElseThrow(
- () -> new IllegalArgumentException(String.format("Could not find a release ticket for %s!", module)));
+ return getTicketsFor(module).getReleaseTicket(module);
}
- private Ticket toTicket(GitHubIssue issue) {
- return new Ticket(issue.getId(), issue.getTitle(), new GithubTicketStatus(issue.getState()));
- }
-
- /**
+ /*
* (non-Javadoc)
*
* @see IssueTracker#findTickets(Project, Collection)
@@ -110,13 +105,13 @@ class GitHubIssueTracker implements IssueTracker {
List tickets = new ArrayList<>();
for (String ticketId : ticketIds) {
- Map parameters = new HashMap<>();
+ Map parameters = newUrlTemplateVariables();
parameters.put("repoName", repositoryName);
parameters.put("id", ticketId);
try {
GitHubIssue gitHubIssue = operations.exchange(ISSUE_BY_ID_URI_TEMPLATE, HttpMethod.GET,
- new HttpEntity<>(getAuthenticationHeaders()), ISSUE_TYPE, parameters).getBody();
+ new HttpEntity<>(newUserScopedHttpHeaders()), ISSUE_TYPE, parameters).getBody();
tickets.add(toTicket(gitHubIssue));
} catch (HttpStatusCodeException e) {
@@ -136,15 +131,15 @@ class GitHubIssueTracker implements IssueTracker {
*/
@Override
@Cacheable("changelogs")
- public Changelog getChangelogFor(ModuleIteration module) {
+ public Changelog getChangelogFor(ModuleIteration moduleIteration) {
- List tickets = getIssuesFor(module).stream().//
+ Tickets tickets = getIssuesFor(moduleIteration, false).stream().//
map(issue -> toTicket(issue)).//
- collect(Collectors.toList());
+ collect(Tickets.toTicketsCollector());
- logger.log(module, "Created changelog with %s entries.", tickets.size());
+ logger.log(moduleIteration, "Created changelog with %s entries.", tickets.getOverallTotal());
- return new Changelog(module, new Tickets(tickets));
+ return new Changelog(moduleIteration, tickets);
}
/*
@@ -156,53 +151,159 @@ class GitHubIssueTracker implements IssueTracker {
return project.uses(Tracker.GITHUB);
}
- private List getIssuesFor(ModuleIteration module) {
+ @Cacheable("tickets")
+ public Tickets getTicketsFor(ModuleIteration iteration) {
+ return getTicketsFor(iteration, false);
+ }
- String repositoryName = GitProject.of(module.getProject()).getRepositoryName();
+ @Override
+ public Tickets getTicketsFor(TrainIteration iteration) {
+ return getTicketsFor(iteration, false);
+ }
- GitHubMilestone milestone = findMilestone(module, repositoryName);
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.release.jira.GitHubIssueConnector#getTicketsFor(org.springframework.data.release.model.TrainIteration, boolean)
+ */
+ @Override
+ public Tickets getTicketsFor(TrainIteration trainIteration, boolean forCurrentUser) {
- Map parameters = new HashMap<>();
+ if (forCurrentUser) {
+ logger.log(trainIteration, "Retrieving tickets (for user %s)…", properties.getUsername());
+ } else {
+ logger.log(trainIteration, "Retrieving tickets…");
+ }
+
+ Tickets tickets = trainIteration.stream(). //
+ filter(moduleIteration -> supports(moduleIteration.getProject())). //
+ flatMap(moduleIteration -> getTicketsFor(moduleIteration, forCurrentUser).stream()). //
+ collect(Tickets.toTicketsCollector());
+
+ return tickets;
+ }
+
+ @Override
+ public void createReleaseVersion(ModuleIteration moduleIteration) {
+
+ Assert.notNull(moduleIteration, "ModuleIteration must not be null.");
+
+ String repositoryName = GitProject.of(moduleIteration.getProject()).getRepositoryName();
+
+ Optional milestone = findMilestone(moduleIteration, repositoryName);
+
+ if (milestone.isPresent()) {
+ return;
+ }
+
+ GithubMilestone githubMilestone = new GithubMilestone(moduleIteration);
+ logger.log(moduleIteration, "Creating GitHub milestone %s", githubMilestone);
+
+ HttpHeaders httpHeaders = newUserScopedHttpHeaders();
+ Map parameters = newUrlTemplateVariables();
+ parameters.put("repoName", repositoryName);
+
+ operations.exchange(MILESTONES_URI_TEMPLATE, HttpMethod.POST,
+ new HttpEntity