#2 - Provide command to list all issue branches alongside their state in the issue tracker
This commit is contained in:
committed by
Oliver Gierke
parent
8abad1385f
commit
67e5b36bc6
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,4 +5,6 @@ target/
|
||||
.factorypath
|
||||
.springBeans
|
||||
application-local.properties
|
||||
spring-shell.log
|
||||
spring-shell.log
|
||||
.idea/
|
||||
*.iml
|
||||
@@ -31,7 +31,7 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
class Branch {
|
||||
public class Branch implements Comparable<Branch> {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Branch> {
|
||||
|
||||
private final List<Branch> 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<Branch> 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<Branch> asList() {
|
||||
return branches;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Iterable#iterator()
|
||||
*/
|
||||
@Override
|
||||
public Iterator<Branch> iterator() {
|
||||
return branches.iterator();
|
||||
}
|
||||
}
|
||||
@@ -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> 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> ticket = ticketBranches.getTicket(branch);
|
||||
table.addRow(branch.toString(), ticket.map(t -> t.getTicketStatus().getLabel()).orElse(""),
|
||||
ticket.map(t -> t.getSummary()).orElse(""));
|
||||
});
|
||||
|
||||
return table;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Ref> branches = git.lsRemote().setHeads(true).setTags(false).call();
|
||||
|
||||
Set<String> 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<Ticket> tickets = tracker.findTickets(project, possibleTicketIds);
|
||||
Map<String, Ticket> ticketMap = tickets.stream().collect(Collectors.toMap(Ticket::getId, ticket -> ticket));
|
||||
|
||||
Map<Branch, Ticket> 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
|
||||
|
||||
@@ -27,6 +27,7 @@ class GitHubIssue {
|
||||
|
||||
private String number;
|
||||
private String title;
|
||||
private String state;
|
||||
|
||||
public String getId() {
|
||||
return "#".concat(number);
|
||||
|
||||
@@ -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<List<GitHubMilestone>> MILESTONES_TYPE = new ParameterizedTypeReference<List<GitHubMilestone>>() {};
|
||||
private static final ParameterizedTypeReference<List<GitHubIssue>> ISSUES_TYPE = new ParameterizedTypeReference<List<GitHubIssue>>() {};
|
||||
private static final ParameterizedTypeReference<List<GitHubMilestone>> MILESTONES_TYPE = new ParameterizedTypeReference<List<GitHubMilestone>>() {
|
||||
};
|
||||
private static final ParameterizedTypeReference<List<GitHubIssue>> ISSUES_TYPE = new ParameterizedTypeReference<List<GitHubIssue>>() {
|
||||
};
|
||||
private static final ParameterizedTypeReference<GitHubIssue> ISSUE_TYPE = new ParameterizedTypeReference<GitHubIssue>() {
|
||||
};
|
||||
|
||||
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<Ticket> findTickets(Project project, Collection<String> ticketIds) {
|
||||
|
||||
String repositoryName = new GitProject(project, new GitServer()).getRepositoryName();
|
||||
List<Ticket> tickets = new ArrayList<>();
|
||||
for (String ticketId : ticketIds) {
|
||||
|
||||
Map<String, Object> 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<Ticket> 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<Project> {
|
||||
|
||||
/**
|
||||
* 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<Project> {
|
||||
*/
|
||||
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<Ticket> findTickets(Project project, Collection<String> ticketIds);
|
||||
|
||||
Changelog getChangelogFor(ModuleIteration iteration);
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String, Object> 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<Ticket> findTickets(Project project, Collection<String> ticketIds) {
|
||||
|
||||
if (ticketIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
JqlQuery query = JqlQuery.from(ticketIds).and(" resolution is not EMPTY");
|
||||
|
||||
Map<String, Object> 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<String, Object> 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<String, Object> 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<Ticket> 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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -52,6 +52,8 @@ class JiraIssue {
|
||||
|
||||
String summary;
|
||||
List<FixVersions> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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<String> parts = new ArrayList<>();
|
||||
|
||||
@@ -27,6 +27,7 @@ public class Ticket {
|
||||
|
||||
private final String id;
|
||||
private final String summary;
|
||||
private final TicketStatus ticketStatus;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
|
||||
@@ -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<Branch> {
|
||||
|
||||
private Map<Branch, Ticket> 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<Branch, Ticket> ticketBranches) {
|
||||
|
||||
Assert.notNull(ticketBranches, "TicketBranches must not be null!");
|
||||
this.ticketBranches = ticketBranches;
|
||||
}
|
||||
|
||||
public static TicketBranches from(Map<Branch, Ticket> ticketBranches) {
|
||||
|
||||
return new TicketBranches(ticketBranches);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Branch> iterator() {
|
||||
return ticketBranches.keySet().iterator();
|
||||
}
|
||||
|
||||
public Stream<Branch> stream() {
|
||||
return StreamSupport.stream(spliterator(), false);
|
||||
}
|
||||
|
||||
public Optional<Ticket> getTicket(Branch branch) {
|
||||
Assert.notNull(branch, "Branch must not be null!");
|
||||
return Optional.ofNullable(ticketBranches.get(branch));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -34,7 +34,7 @@ public class Project implements Comparable<Project> {
|
||||
private final @Getter ProjectKey key;
|
||||
private final @Getter String name;
|
||||
private final @Getter List<Project> dependencies;
|
||||
private final Tracker tracker;
|
||||
private final @Getter Tracker tracker;
|
||||
private final @Getter ArtifactCoordinates additionalArtifacts;
|
||||
|
||||
Project(String key, String name, List<Project> dependencies) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -54,4 +54,9 @@ public class GitOperationsIntegrationTests extends AbstractIntegrationTests {
|
||||
public void obtainsVersionTagsForRepoThatAlsoHasOtherTags() {
|
||||
gitOperations.getTags(MONGO_DB);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getResolvedBranches() {
|
||||
gitOperations.listTicketBranches(REDIS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Ticket> tickets = gitHubIssueTracker.findTickets(Projects.BUILD, Arrays.asList("1", "2", "-1"));
|
||||
assertThat(tickets, hasSize(2));
|
||||
}
|
||||
}
|
||||
@@ -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<Ticket> tickets = jira.findTickets(Projects.COMMONS, Arrays.asList("DATAREDIS-1", "DATAJPA-1"));
|
||||
assertThat(tickets, hasSize(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ignoresUnknownTicketsByTicketId() throws Exception {
|
||||
|
||||
Collection<Ticket> tickets = jira.findTickets(Projects.COMMONS, Arrays.asList("XYZ-1", "UNKOWN-1"));
|
||||
assertThat(tickets, hasSize(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyResultWithEmptyTicketIds() throws Exception {
|
||||
|
||||
Collection<Ticket> tickets = jira.findTickets(Projects.COMMONS, Arrays.asList());
|
||||
assertThat(tickets, hasSize(0));
|
||||
}
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user