#2 - Provide command to list all issue branches alongside their state in the issue tracker

This commit is contained in:
Mark Paluch
2016-02-11 08:34:02 +01:00
committed by Oliver Gierke
parent 8abad1385f
commit 67e5b36bc6
24 changed files with 768 additions and 36 deletions

4
.gitignore vendored
View File

@@ -5,4 +5,6 @@ target/
.factorypath
.springBeans
application-local.properties
spring-shell.log
spring-shell.log
.idea/
*.iml

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -27,6 +27,7 @@ class GitHubIssue {
private String number;
private String title;
private String state;
public String getId() {
return "#".concat(number);

View File

@@ -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();
}

View File

@@ -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 {
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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());

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<>();

View File

@@ -27,6 +27,7 @@ public class Ticket {
private final String id;
private final String summary;
private final TicketStatus ticketStatus;
/*
* (non-Javadoc)

View File

@@ -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));
}
}

View File

@@ -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();
}

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -54,4 +54,9 @@ public class GitOperationsIntegrationTests extends AbstractIntegrationTests {
public void obtainsVersionTagsForRepoThatAlsoHasOtherTags() {
gitOperations.getTags(MONGO_DB);
}
@Test
public void getResolvedBranches() {
gitOperations.listTicketBranches(REDIS);
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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")));
}
}