diff --git a/.gitignore b/.gitignore index 48b281b..1b29e90 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ target/ .factorypath .springBeans application-local.properties -spring-shell.log \ No newline at end of file +spring-shell.log +.idea/ +*.iml \ No newline at end of file diff --git a/release-tools/src/main/java/org/springframework/data/release/git/Branch.java b/release-tools/src/main/java/org/springframework/data/release/git/Branch.java index f3919d6..630f83b 100644 --- a/release-tools/src/main/java/org/springframework/data/release/git/Branch.java +++ b/release-tools/src/main/java/org/springframework/data/release/git/Branch.java @@ -31,7 +31,7 @@ import org.springframework.util.Assert; */ @EqualsAndHashCode @RequiredArgsConstructor(access = AccessLevel.PRIVATE) -class Branch { +public class Branch implements Comparable { public static final Branch MASTER = new Branch("master"); @@ -73,7 +73,7 @@ class Branch { return MASTER.equals(this); } - /* + /* * (non-Javadoc) * @see java.lang.Object#toString() */ @@ -81,4 +81,9 @@ class Branch { public String toString() { return name; } + + @Override + public int compareTo(Branch o) { + return name.compareToIgnoreCase(o.name); + } } diff --git a/release-tools/src/main/java/org/springframework/data/release/git/Branches.java b/release-tools/src/main/java/org/springframework/data/release/git/Branches.java new file mode 100644 index 0000000..1e72ed4 --- /dev/null +++ b/release-tools/src/main/java/org/springframework/data/release/git/Branches.java @@ -0,0 +1,69 @@ +/* + * 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.git; + +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; + +import org.springframework.data.release.model.ArtifactVersion; +import org.springframework.data.release.model.ModuleIteration; +import org.springframework.util.Assert; + +/** + * Value object to represent a collection of {@link Branch}es. + * + * @author Mark Paluch + */ +@EqualsAndHashCode +public class Branches implements Iterable { + + private final List branches; + + /** + * Creates a new {@link Branches} instance for the given {@link List} of {@link Branch}es. + * + * @param source must not be {@literal null}. + */ + Branches(List source) { + + Assert.notNull(source, "Tags must not be null!"); + + this.branches = source.stream().// + sorted().collect(Collectors.toList()); + } + + + /** + * Returns all {@link Branch}es as {@link List}. + * + * @return + */ + public List asList() { + return branches; + } + + /* + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + @Override + public Iterator iterator() { + return branches.iterator(); + } +} diff --git a/release-tools/src/main/java/org/springframework/data/release/git/GitCommands.java b/release-tools/src/main/java/org/springframework/data/release/git/GitCommands.java index 994fa17..540b0ff 100644 --- a/release-tools/src/main/java/org/springframework/data/release/git/GitCommands.java +++ b/release-tools/src/main/java/org/springframework/data/release/git/GitCommands.java @@ -15,7 +15,7 @@ */ package org.springframework.data.release.git; -import lombok.RequiredArgsConstructor; +import java.util.Optional; import java.util.List; import java.util.stream.Collectors; @@ -23,6 +23,8 @@ import java.util.stream.Stream; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.release.CliComponent; +import org.springframework.data.release.jira.Ticket; +import org.springframework.data.release.jira.TicketBranches; import org.springframework.data.release.model.Project; import org.springframework.data.release.model.ReleaseTrains; import org.springframework.data.release.model.Train; @@ -30,8 +32,12 @@ import org.springframework.data.release.model.TrainIteration; import org.springframework.shell.core.CommandMarker; import org.springframework.shell.core.annotation.CliCommand; import org.springframework.shell.core.annotation.CliOption; +import org.springframework.shell.support.table.Table; +import org.springframework.shell.support.table.TableHeader; import org.springframework.util.StringUtils; +import lombok.RequiredArgsConstructor; + /** * @author Oliver Gierke */ @@ -67,8 +73,8 @@ public class GitCommands implements CommandMarker { /** * Resets all projects contained in the given {@link Train}. - * - * @param trainName + * + * @param iteration * @throws Exception */ @CliCommand("git reset") @@ -84,7 +90,7 @@ public class GitCommands implements CommandMarker { /** * Pushes all changes of all modules of the given {@link TrainIteration} to the remote server. If {@code tags} is * given, only the tags are pushed. - * + * * @param iteration * @param tags * @throws Exception @@ -118,4 +124,44 @@ public class GitCommands implements CommandMarker { git.backportChangelogs(iteration, targets); } + + /** + * List the branches with their tickets of the git repository. + * + * @param projectName + * @return + * @throws Exception + */ + @CliCommand("git issuebranches") + public Table issuebranches(@CliOption(key = { "" }, mandatory = true) String projectName, + @CliOption(key = "resolved") Boolean resolved) throws Exception { + + Project project = ReleaseTrains.getProjectByName(projectName); + + Table table = new Table(); + TicketBranches ticketBranches = git.listTicketBranches(project); + + table.addHeader(1, new TableHeader("Branch")); + table.addHeader(2, new TableHeader("Status")); + table.addHeader(3, new TableHeader("Description")); + + ticketBranches.stream().sorted().// + filter(branch -> { + Optional ticket = ticketBranches.getTicket(branch); + if (resolved != null && resolved) { + if (!ticket.isPresent() || (ticket.isPresent() && ticket.get().getTicketStatus().isResolved())) { + return false; + } + } + + return true; + }).// + forEachOrdered(branch -> { + Optional ticket = ticketBranches.getTicket(branch); + table.addRow(branch.toString(), ticket.map(t -> t.getTicketStatus().getLabel()).orElse(""), + ticket.map(t -> t.getSummary()).orElse("")); + }); + + return table; + } } diff --git a/release-tools/src/main/java/org/springframework/data/release/git/GitOperations.java b/release-tools/src/main/java/org/springframework/data/release/git/GitOperations.java index bc0d75b..9dc3df3 100644 --- a/release-tools/src/main/java/org/springframework/data/release/git/GitOperations.java +++ b/release-tools/src/main/java/org/springframework/data/release/git/GitOperations.java @@ -15,19 +15,26 @@ */ package org.springframework.data.release.git; -import lombok.RequiredArgsConstructor; - import java.io.File; import java.io.IOException; +import java.util.Collection; import java.util.List; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; + import org.eclipse.jgit.api.CheckoutCommand; import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.ResetCommand.ResetType; import org.eclipse.jgit.api.errors.RefNotFoundException; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; @@ -40,6 +47,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.release.io.Workspace; import org.springframework.data.release.jira.IssueTracker; import org.springframework.data.release.jira.Ticket; +import org.springframework.data.release.jira.TicketBranches; import org.springframework.data.release.model.ArtifactVersion; import org.springframework.data.release.model.ModuleIteration; import org.springframework.data.release.model.Project; @@ -93,7 +101,7 @@ public class GitOperations { /** * Checks out all projects of the given {@link TrainIteration}. - * + * * @param train * @throws Exception */ @@ -269,6 +277,63 @@ public class GitOperations { }); } + /** + * Retrieve a list of remote branches where their related ticket is resolved. + * + * @param project + * @return + */ + public TicketBranches listTicketBranches(Project project) { + + IssueTracker tracker = issueTracker.getPluginFor(project); + + try (Git git = new Git(getRepository(project))) { + update(project); + Pattern pattern = Pattern.compile(project.getTracker().getTicketPattern()); + + Collection branches = git.lsRemote().setHeads(true).setTags(false).call(); + + Set possibleTicketIds = branches.stream().// + filter(branch -> pattern.matcher(branch.getName()).find()).// + map(branch -> { + Matcher matcher = pattern.matcher(branch.getName()); + matcher.find(); + return matcher.group(1); + }).// + collect(Collectors.toSet()); + + Collection tickets = tracker.findTickets(project, possibleTicketIds); + Map ticketMap = tickets.stream().collect(Collectors.toMap(Ticket::getId, ticket -> ticket)); + + Map ticketBranches = new HashMap<>(); + branches.stream().// + map(branch -> { + if (branch.getName().startsWith(Constants.R_HEADS)) { + return branch.getName().substring(Constants.R_HEADS.length()); + } + + if (branch.getName().startsWith(Constants.R_REMOTES)) { + return branch.getName().substring(Constants.R_REMOTES.length()); + } + return branch.getName(); + }).filter(branchName -> { + Matcher matcher = pattern.matcher(branchName); + return matcher.find(); + }).// + forEach((branchName) -> { + + Matcher matcher = pattern.matcher(branchName); + matcher.find(); + String ticketId = matcher.group(1); + ticketBranches.put(Branch.from(branchName), ticketMap.get(ticketId)); + }); + + return TicketBranches.from(ticketBranches); + } catch (Exception o_O) { + throw new RuntimeException(o_O); + } + } + public void tagRelease(TrainIteration iteration) { ExecutionUtils.run(iteration, module -> { @@ -300,7 +365,7 @@ public class GitOperations { /** * Commits all changes currently made to all modules of the given {@link TrainIteration}. The summary can contain a * single {@code %s} placeholder which the version of the current module will get replace into. - * + * * @param iteration must not be {@literal null}. * @param summary must not be {@literal null} or empty. * @throws Exception @@ -328,7 +393,7 @@ public class GitOperations { /** * Commits the given files for the given {@link ModuleIteration} using the given summary for the commit message. If no * files are given, all pending changes are committed. - * + * * @param module must not be {@literal null}. * @param summary must not be {@literal null} or empty. * @param files can be empty. @@ -505,7 +570,7 @@ public class GitOperations { * starting with the release ticket identifier, followed by a dash separated by spaces and the key word * {@code Release}. To prevent skimming through the entire Git history, we expect such a commit to be found within the * 50 most recent commits. - * + * * @param module * @return * @throws Exception diff --git a/release-tools/src/main/java/org/springframework/data/release/jira/GitHubIssue.java b/release-tools/src/main/java/org/springframework/data/release/jira/GitHubIssue.java index 3a8f738..8afb420 100644 --- a/release-tools/src/main/java/org/springframework/data/release/jira/GitHubIssue.java +++ b/release-tools/src/main/java/org/springframework/data/release/jira/GitHubIssue.java @@ -27,6 +27,7 @@ class GitHubIssue { private String number; private String title; + private String state; public String getId() { return "#".concat(number); diff --git a/release-tools/src/main/java/org/springframework/data/release/jira/GitHubIssueTracker.java b/release-tools/src/main/java/org/springframework/data/release/jira/GitHubIssueTracker.java index 8f5ee2d..b3a3af9 100644 --- a/release-tools/src/main/java/org/springframework/data/release/jira/GitHubIssueTracker.java +++ b/release-tools/src/main/java/org/springframework/data/release/jira/GitHubIssueTracker.java @@ -15,8 +15,6 @@ */ package org.springframework.data.release.jira; -import lombok.RequiredArgsConstructor; - import java.net.URI; import java.util.Arrays; import java.util.HashMap; @@ -24,6 +22,9 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; + +import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; @@ -41,6 +42,8 @@ import org.springframework.data.release.utils.Logger; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestOperations; import org.springframework.web.util.UriTemplate; @@ -48,19 +51,34 @@ import org.springframework.web.util.UriTemplate; * @author Oliver Gierke */ @RequiredArgsConstructor -class GitHubIssueTracker implements IssueTracker { +class GitHubIssueTracker implements GitHubConnector { private static final String MILESTONE_URI = "https://api.github.com/repos/spring-projects/{repoName}/milestones?state={state}"; - private static final String URI_TEMPLATE = "https://api.github.com/repos/spring-projects/{repoName}/issues?milestone={id}&state=all"; + 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 ParameterizedTypeReference> MILESTONES_TYPE = new ParameterizedTypeReference>() {}; - private static final ParameterizedTypeReference> ISSUES_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() { + }; private final RestOperations operations; private final Logger logger; private final GitProperties properties; - /* + /* + * (non-Javadoc) + * @see org.springframework.data.release.jira.JiraConnector#flushTickets() + */ + @Override + @CacheEvict(value = "tickets", allEntries = true) + public void reset() { + + } + + /* * (non-Javadoc) * @see org.springframework.data.release.jira.IssueTracker#getReleaseTicketFor(org.springframework.data.release.model.ModuleIteration) */ @@ -71,12 +89,50 @@ class GitHubIssueTracker implements IssueTracker { return getIssuesFor(module).stream().// filter(issue -> issue.isReleaseTicket(module)).// findFirst().// - map(issue -> new Ticket(issue.getId(), issue.getTitle())).// + map(issue -> toTicket(issue)).// orElseThrow( - () -> new IllegalArgumentException(String.format("Could not find a release ticket for %s!", module))); + () -> new IllegalArgumentException(String.format("Could not find a release ticket for %s!", module))); } - /* + private Ticket toTicket(GitHubIssue issue) { + return new Ticket(issue.getId(), issue.getTitle(), new GithubTicketStatus(issue.getState())); + } + + /** + * (non-Javadoc) + * + * @see IssueTracker#findTickets(Project, Collection) + */ + @Override + @Cacheable("tickets") + public Collection findTickets(Project project, Collection ticketIds) { + + String repositoryName = new GitProject(project, new GitServer()).getRepositoryName(); + List tickets = new ArrayList<>(); + for (String ticketId : ticketIds) { + + Map parameters = new HashMap<>(); + 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(); + + tickets.add(toTicket(gitHubIssue)); + } catch (HttpStatusCodeException e) { + if (e.getStatusCode() == HttpStatus.NOT_FOUND) { + continue; + } + throw e; + } + } + + return tickets; + } + + /* * (non-Javadoc) * @see org.springframework.data.release.jira.IssueTracker#getChangelogFor(org.springframework.data.release.model.ModuleIteration) */ @@ -85,7 +141,7 @@ class GitHubIssueTracker implements IssueTracker { public Changelog getChangelogFor(ModuleIteration module) { List tickets = getIssuesFor(module).stream().// - map(issue -> new Ticket(issue.getId(), issue.getTitle())).// + map(issue -> toTicket(issue)).// collect(Collectors.toList()); logger.log(module, "Created changelog with %s entries.", tickets.size()); @@ -113,7 +169,7 @@ class GitHubIssueTracker implements IssueTracker { parameters.put("id", milestone.getNumber()); return operations - .exchange(URI_TEMPLATE, HttpMethod.GET, new HttpEntity<>(getAuthenticationHeaders()), ISSUES_TYPE, parameters) + .exchange(ISSUES_BY_MILESTONE_URI_TEMPLATE, HttpMethod.GET, new HttpEntity<>(getAuthenticationHeaders()), ISSUES_TYPE, parameters) .getBody(); } diff --git a/release-tools/src/main/java/org/springframework/data/release/jira/GithubConnector.java b/release-tools/src/main/java/org/springframework/data/release/jira/GithubConnector.java new file mode 100644 index 0000000..58e5ac7 --- /dev/null +++ b/release-tools/src/main/java/org/springframework/data/release/jira/GithubConnector.java @@ -0,0 +1,27 @@ +/* + * 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 org.springframework.data.release.model.Iteration; +import org.springframework.data.release.model.Train; +import org.springframework.data.release.model.TrainIteration; + +/** + * @author Mark Paluch + */ +public interface GitHubConnector extends IssueTracker { + +} diff --git a/release-tools/src/main/java/org/springframework/data/release/jira/GithubTicketStatus.java b/release-tools/src/main/java/org/springframework/data/release/jira/GithubTicketStatus.java new file mode 100644 index 0000000..f0533b0 --- /dev/null +++ b/release-tools/src/main/java/org/springframework/data/release/jira/GithubTicketStatus.java @@ -0,0 +1,39 @@ +/* + * 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.AllArgsConstructor; + +/** + * Value object for a GitHub ticket status. + * @author Mark Paluch + */ +@AllArgsConstructor +class GithubTicketStatus implements TicketStatus { + + private final String status; + + @Override + public String getLabel() { + return status; + } + + @Override + public boolean isResolved() { + return "closed".equalsIgnoreCase(status); + } +} diff --git a/release-tools/src/main/java/org/springframework/data/release/jira/IssueTracker.java b/release-tools/src/main/java/org/springframework/data/release/jira/IssueTracker.java index cce5634..a846e21 100644 --- a/release-tools/src/main/java/org/springframework/data/release/jira/IssueTracker.java +++ b/release-tools/src/main/java/org/springframework/data/release/jira/IssueTracker.java @@ -15,6 +15,8 @@ */ package org.springframework.data.release.jira; +import java.util.Collection; + import org.springframework.data.release.model.ModuleIteration; import org.springframework.data.release.model.Project; import org.springframework.plugin.core.Plugin; @@ -24,6 +26,11 @@ import org.springframework.plugin.core.Plugin; */ public interface IssueTracker extends Plugin { + /** + * Reset internal state (cache, ...). + */ + void reset(); + /** * Returns the {@link Ticket} that tracks modifications in the context of a release. * @@ -32,5 +39,16 @@ public interface IssueTracker extends Plugin { */ Ticket getReleaseTicketFor(ModuleIteration module); + /** + * Query the issue tracker for multiple {@link Ticket#id ticket Ids}. Tickets that are not found are not returned + * within the result. + * + * @param project + * @param ticketIds collection of {@link Ticket#id ticket Ids} + * @return + */ + Collection findTickets(Project project, Collection ticketIds); + Changelog getChangelogFor(ModuleIteration iteration); + } diff --git a/release-tools/src/main/java/org/springframework/data/release/jira/Jira.java b/release-tools/src/main/java/org/springframework/data/release/jira/Jira.java index 10c02d3..2c2b98a 100644 --- a/release-tools/src/main/java/org/springframework/data/release/jira/Jira.java +++ b/release-tools/src/main/java/org/springframework/data/release/jira/Jira.java @@ -15,14 +15,16 @@ */ package org.springframework.data.release.jira; -import lombok.RequiredArgsConstructor; - import java.net.URI; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; + +import lombok.RequiredArgsConstructor; import org.springframework.boot.SpringApplication; import org.springframework.cache.annotation.CacheEvict; @@ -71,7 +73,7 @@ class Jira implements JiraConnector { Map parameters = new HashMap<>(); parameters.put("jql", query); - parameters.put("fields", "summary"); + parameters.put("fields", "summary,status,resolution"); parameters.put("startAt", 0); JiraIssues issues = operations.exchange(SEARCH_TEMPLATE, HttpMethod.GET, null, JiraIssues.class, parameters) @@ -83,7 +85,34 @@ class Jira implements JiraConnector { JiraIssue issue = issues.getIssues().get(0); - return new Ticket(issue.getKey(), issue.getFields().getSummary()); + return toTicket(issue); + } + + /** + * (non-Javadoc) + * @see org.springframework.data.release.jira.IssueTracker#findTickets(Project, Collection) + */ + @Override + @Cacheable("tickets") + public Collection findTickets(Project project, Collection ticketIds) { + + if (ticketIds.isEmpty()) { + return Collections.emptyList(); + } + + JqlQuery query = JqlQuery.from(ticketIds).and(" resolution is not EMPTY"); + + Map parameters = new HashMap<>(); + parameters.put("jql", query); + parameters.put("fields", "summary,status,resolution"); + parameters.put("startAt", 0); + + JiraIssues issues = operations.exchange(SEARCH_TEMPLATE, HttpMethod.GET, null, JiraIssues.class, parameters) + .getBody(); + + return issues.getIssues().stream().// + map(this::toTicket).// + collect(Collectors.toList()); } /* @@ -118,7 +147,7 @@ class Jira implements JiraConnector { Map parameters = new HashMap<>(); parameters.put("jql", query); - parameters.put("fields", "summary,fixVersions"); + parameters.put("fields", "summary,status,resolution,fixVersions"); parameters.put("startAt", startAt); issues = operations @@ -128,7 +157,7 @@ class Jira implements JiraConnector { for (JiraIssue issue : issues) { if (!issue.wasBackportedFrom(iteration.getTrain())) { - tickets.add(new Ticket(issue.getKey(), issue.getFields().getSummary())); + tickets.add(toTicket(issue)); } } @@ -139,6 +168,24 @@ class Jira implements JiraConnector { return new Tickets(Collections.unmodifiableList(tickets), issues.getTotal()); } + private Ticket toTicket(JiraIssue issue) { + + JiraIssue.Fields fields = issue.getFields(); + + JiraTicketStatus jiraTicketStatus; + if (fields.getStatus() != null && fields.getResolution() != null) { + JiraIssue.Status status = fields.getStatus(); + boolean resolved = status.getStatusCategory().getKey().equals("done"); + JiraIssue.Resolution resolution = fields.getResolution(); + + jiraTicketStatus = new JiraTicketStatus(resolved, status.getName(), resolution.getName()); + } else { + jiraTicketStatus = JiraTicketStatus.UNKNOWN; + } + + return new Ticket(issue.getKey(), fields.getSummary(), jiraTicketStatus); + } + /* * (non-Javadoc) * @see org.springframework.data.release.jira.JiraConnector#verifyBeforeRelease(org.springframework.data.release.model.Train, org.springframework.data.release.model.Iteration) @@ -180,7 +227,7 @@ class Jira implements JiraConnector { Map parameters = new HashMap<>(); parameters.put("jql", JqlQuery.from(module)); - parameters.put("fields", "summary,fixVersions"); + parameters.put("fields", "summary,status,resolution,fixVersions"); parameters.put("startAt", 0); URI searchUri = new UriTemplate(SEARCH_TEMPLATE).expand(parameters); @@ -191,7 +238,7 @@ class Jira implements JiraConnector { List tickets = new ArrayList<>(); for (JiraIssue issue : issues) { - tickets.add(new Ticket(issue.getKey(), issue.getFields().getSummary())); + tickets.add(toTicket(issue)); } logger.log(module, "Created changelog with %s entries.", tickets.size()); diff --git a/release-tools/src/main/java/org/springframework/data/release/jira/JiraConnector.java b/release-tools/src/main/java/org/springframework/data/release/jira/JiraConnector.java index 21b638f..b24ef73 100644 --- a/release-tools/src/main/java/org/springframework/data/release/jira/JiraConnector.java +++ b/release-tools/src/main/java/org/springframework/data/release/jira/JiraConnector.java @@ -29,8 +29,8 @@ public interface JiraConnector extends IssueTracker { /** * Returns all {@link Tickets} for the given {@link Train} and {@link Iteration}. * - * @param train must not be {@literal null}. * @param iteration must not be {@literal null}. + * @param credentials may be {@literal null}. * @return */ Tickets getTicketsFor(TrainIteration iteration, Credentials credentials); diff --git a/release-tools/src/main/java/org/springframework/data/release/jira/JiraIssue.java b/release-tools/src/main/java/org/springframework/data/release/jira/JiraIssue.java index b90f3f2..7779932 100644 --- a/release-tools/src/main/java/org/springframework/data/release/jira/JiraIssue.java +++ b/release-tools/src/main/java/org/springframework/data/release/jira/JiraIssue.java @@ -52,6 +52,8 @@ class JiraIssue { String summary; List fixVersions; + Status status; + Resolution resolution; } @Data @@ -61,4 +63,29 @@ class JiraIssue { String name; } + + @Data + @NoArgsConstructor + @AllArgsConstructor + static class Status { + + String name; + StatusCategory statusCategory; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + static class Resolution { + + String name; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + static class StatusCategory { + + String key; + } } diff --git a/release-tools/src/main/java/org/springframework/data/release/jira/JiraTicketStatus.java b/release-tools/src/main/java/org/springframework/data/release/jira/JiraTicketStatus.java new file mode 100644 index 0000000..c3782ca --- /dev/null +++ b/release-tools/src/main/java/org/springframework/data/release/jira/JiraTicketStatus.java @@ -0,0 +1,42 @@ +/* + * 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.AllArgsConstructor; + +/** + * @author Mark Paluch + */ +@AllArgsConstructor +public class JiraTicketStatus implements TicketStatus { + + public static final JiraTicketStatus UNKNOWN = new JiraTicketStatus(false, "unknown", null); + + private final boolean resolved; + private final String status; + private final String resolution; + + @Override + public String getLabel() { + return resolution == null ? status : status + "/" + resolution; + } + + @Override + public boolean isResolved() { + return resolved; + } +} diff --git a/release-tools/src/main/java/org/springframework/data/release/jira/JqlQuery.java b/release-tools/src/main/java/org/springframework/data/release/jira/JqlQuery.java index b8dd9e2..bef4f93 100644 --- a/release-tools/src/main/java/org/springframework/data/release/jira/JqlQuery.java +++ b/release-tools/src/main/java/org/springframework/data/release/jira/JqlQuery.java @@ -20,7 +20,10 @@ import static org.springframework.data.release.model.Projects.*; import lombok.Value; import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.StringJoiner; +import java.util.stream.Collectors; import org.springframework.data.release.model.ModuleIteration; import org.springframework.data.release.model.TrainIteration; @@ -33,6 +36,7 @@ import org.springframework.util.StringUtils; class JqlQuery { private static final String PROJECT_VERSION_TEMPLATE = "project = %s AND fixVersion = \"%s\""; + private static final String ISSUE_KEY_IN_TEMPLATE = "issueKey in (%s)"; private final String query; @@ -51,6 +55,13 @@ class JqlQuery { return new JqlQuery(String.format(PROJECT_VERSION_TEMPLATE, iteration.getProjectKey(), version)); } + public static JqlQuery from(Collection ticketIds) { + + String joinedTicketIds = ticketIds.stream().collect(Collectors.joining(", ")); + + return new JqlQuery(String.format(ISSUE_KEY_IN_TEMPLATE, joinedTicketIds)); + } + public static JqlQuery from(TrainIteration iteration) { List parts = new ArrayList<>(); diff --git a/release-tools/src/main/java/org/springframework/data/release/jira/Ticket.java b/release-tools/src/main/java/org/springframework/data/release/jira/Ticket.java index d178bca..ec8a99e 100644 --- a/release-tools/src/main/java/org/springframework/data/release/jira/Ticket.java +++ b/release-tools/src/main/java/org/springframework/data/release/jira/Ticket.java @@ -27,6 +27,7 @@ public class Ticket { private final String id; private final String summary; + private final TicketStatus ticketStatus; /* * (non-Javadoc) diff --git a/release-tools/src/main/java/org/springframework/data/release/jira/TicketBranches.java b/release-tools/src/main/java/org/springframework/data/release/jira/TicketBranches.java new file mode 100644 index 0000000..822189d --- /dev/null +++ b/release-tools/src/main/java/org/springframework/data/release/jira/TicketBranches.java @@ -0,0 +1,69 @@ +/* + * 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 java.util.Iterator; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import lombok.EqualsAndHashCode; + +import org.springframework.data.release.git.Branch; +import org.springframework.util.Assert; + +/** + * Value object to represent a collection of {@link Branch}es with optionally assigned tickets. + * + * @author Mark Paluch + */ +@EqualsAndHashCode +public class TicketBranches implements Iterable { + + private Map ticketBranches; + + /** + * Creates a new {@link TicketBranches} instance for the given {@link Map} of {@link Branch}es and {@link Ticket}s. + * + * @param ticketBranches must not be {@literal null}. + */ + TicketBranches(Map ticketBranches) { + + Assert.notNull(ticketBranches, "TicketBranches must not be null!"); + this.ticketBranches = ticketBranches; + } + + public static TicketBranches from(Map ticketBranches) { + + return new TicketBranches(ticketBranches); + } + + @Override + public Iterator iterator() { + return ticketBranches.keySet().iterator(); + } + + public Stream stream() { + return StreamSupport.stream(spliterator(), false); + } + + public Optional getTicket(Branch branch) { + Assert.notNull(branch, "Branch must not be null!"); + return Optional.ofNullable(ticketBranches.get(branch)); + } +} diff --git a/release-tools/src/main/java/org/springframework/data/release/jira/TicketStatus.java b/release-tools/src/main/java/org/springframework/data/release/jira/TicketStatus.java new file mode 100644 index 0000000..081a122 --- /dev/null +++ b/release-tools/src/main/java/org/springframework/data/release/jira/TicketStatus.java @@ -0,0 +1,27 @@ +/* + * 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; + +/** + * @author Mark Paluch + */ +public interface TicketStatus { + + String getLabel(); + + boolean isResolved(); +} diff --git a/release-tools/src/main/java/org/springframework/data/release/model/Project.java b/release-tools/src/main/java/org/springframework/data/release/model/Project.java index 23786fb..f83e350 100644 --- a/release-tools/src/main/java/org/springframework/data/release/model/Project.java +++ b/release-tools/src/main/java/org/springframework/data/release/model/Project.java @@ -34,7 +34,7 @@ public class Project implements Comparable { private final @Getter ProjectKey key; private final @Getter String name; private final @Getter List dependencies; - private final Tracker tracker; + private final @Getter Tracker tracker; private final @Getter ArtifactCoordinates additionalArtifacts; Project(String key, String name, List dependencies) { diff --git a/release-tools/src/main/java/org/springframework/data/release/model/Tracker.java b/release-tools/src/main/java/org/springframework/data/release/model/Tracker.java index 4383523..b45524a 100644 --- a/release-tools/src/main/java/org/springframework/data/release/model/Tracker.java +++ b/release-tools/src/main/java/org/springframework/data/release/model/Tracker.java @@ -20,5 +20,18 @@ package org.springframework.data.release.model; */ public enum Tracker { - JIRA, GITHUB; + JIRA("(([A-Z]{1,10})+-\\d+)"), + GITHUB("((#)?\\d+)"); + + private String ticketPattern; + + Tracker(String ticketPattern) { + this.ticketPattern = ticketPattern; + } + + public String getTicketPattern() + { + return ticketPattern; + } + } diff --git a/release-tools/src/test/java/org/springframework/data/release/git/GitOperationsIntegrationTests.java b/release-tools/src/test/java/org/springframework/data/release/git/GitOperationsIntegrationTests.java index 320c127..b58a4f5 100644 --- a/release-tools/src/test/java/org/springframework/data/release/git/GitOperationsIntegrationTests.java +++ b/release-tools/src/test/java/org/springframework/data/release/git/GitOperationsIntegrationTests.java @@ -54,4 +54,9 @@ public class GitOperationsIntegrationTests extends AbstractIntegrationTests { public void obtainsVersionTagsForRepoThatAlsoHasOtherTags() { gitOperations.getTags(MONGO_DB); } + + @Test + public void getResolvedBranches() { + gitOperations.listTicketBranches(REDIS); + } } diff --git a/release-tools/src/test/java/org/springframework/data/release/jira/GitHubIssueTrackerIntegrationTests.java b/release-tools/src/test/java/org/springframework/data/release/jira/GitHubIssueTrackerIntegrationTests.java new file mode 100644 index 0000000..c1a88ec --- /dev/null +++ b/release-tools/src/test/java/org/springframework/data/release/jira/GitHubIssueTrackerIntegrationTests.java @@ -0,0 +1,43 @@ +/* + * 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 static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.junit.Assert.assertThat; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.release.AbstractIntegrationTests; +import org.springframework.data.release.model.Projects; + +/** + * @author Mark Paluch + */ +public class GitHubIssueTrackerIntegrationTests extends AbstractIntegrationTests { + + @Autowired GitHubConnector gitHubIssueTracker; + + @Test + public void getTickets() throws Exception { + + Collection tickets = gitHubIssueTracker.findTickets(Projects.BUILD, Arrays.asList("1", "2", "-1")); + assertThat(tickets, hasSize(2)); + } +} diff --git a/release-tools/src/test/java/org/springframework/data/release/jira/JiraIntegrationTests.java b/release-tools/src/test/java/org/springframework/data/release/jira/JiraIntegrationTests.java new file mode 100644 index 0000000..9a83915 --- /dev/null +++ b/release-tools/src/test/java/org/springframework/data/release/jira/JiraIntegrationTests.java @@ -0,0 +1,57 @@ +/* + * 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 static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertThat; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.release.AbstractIntegrationTests; +import org.springframework.data.release.model.Projects; + +/** + * @author Mark Paluch + */ +public class JiraIntegrationTests extends AbstractIntegrationTests { + + @Autowired JiraConnector jira; + + @Test + public void findResolvedTicketsByTicketIds() throws Exception { + + Collection tickets = jira.findTickets(Projects.COMMONS, Arrays.asList("DATAREDIS-1", "DATAJPA-1")); + assertThat(tickets, hasSize(2)); + } + + @Test + public void ignoresUnknownTicketsByTicketId() throws Exception { + + Collection tickets = jira.findTickets(Projects.COMMONS, Arrays.asList("XYZ-1", "UNKOWN-1")); + assertThat(tickets, hasSize(0)); + } + + @Test + public void emptyResultWithEmptyTicketIds() throws Exception { + + Collection tickets = jira.findTickets(Projects.COMMONS, Arrays.asList()); + assertThat(tickets, hasSize(0)); + } +} diff --git a/release-tools/src/test/java/org/springframework/data/release/model/TrackerTest.java b/release-tools/src/test/java/org/springframework/data/release/model/TrackerTest.java new file mode 100644 index 0000000..e86c76f --- /dev/null +++ b/release-tools/src/test/java/org/springframework/data/release/model/TrackerTest.java @@ -0,0 +1,62 @@ +/* + * 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.model; + +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.junit.Test; + +/** + * @author Mark Paluch + */ +public class TrackerTest { + + @Test + public void testMatchesTicketNumber() throws Exception { + + Pattern pattern = Pattern.compile(Tracker.JIRA.getTicketPattern()); + Matcher matcher = pattern.matcher("DATAREDIS-1"); + matcher.find(); + + assertThat(matcher.group(1), is(equalTo("DATAREDIS-1"))); + } + + @Test + public void testMatchesBranchNamedLikeTicket() throws Exception { + + Pattern pattern = Pattern.compile(Tracker.JIRA.getTicketPattern()); + Matcher matcher = pattern.matcher("issues/DATAREDIS-1-dummy"); + matcher.find(); + + assertThat(matcher.group(1), is(equalTo("DATAREDIS-1"))); + } + + @Test + public void testVersionBranch() throws Exception { + + Pattern pattern = Pattern.compile(Tracker.GITHUB.getTicketPattern()); + Matcher matcher = pattern.matcher("1.2.x"); + matcher.find(); + + assertThat(matcher.group(1), is(equalTo("1"))); + } +}