#99 - Extract ticket references from Git commits.

This commit is contained in:
Mark Paluch
2020-11-09 16:07:09 +01:00
parent 314878c7a0
commit 4a6f430c3b
15 changed files with 944 additions and 24 deletions

View File

@@ -63,7 +63,7 @@ public class Repository {
return "snapshot";
}
if (version.isMilestoneVersion()) {
if (version.isMilestoneVersion() || version.isReleaseCandidateVersion()) {
return "milestone";
}

View File

@@ -21,16 +21,21 @@ import lombok.RequiredArgsConstructor;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.TimedCommand;
import org.springframework.data.release.issues.Changelog;
import org.springframework.data.release.issues.Ticket;
import org.springframework.data.release.issues.TicketReference;
import org.springframework.data.release.issues.Tickets;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.ReleaseTrains;
import org.springframework.data.release.model.Train;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.ExecutionUtils;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.springframework.shell.support.table.Table;
@@ -46,6 +51,7 @@ import org.springframework.util.StringUtils;
class GitCommands extends TimedCommand {
private final @NonNull GitOperations git;
private final @NonNull Executor executor;
@CliCommand("git co-train")
public void checkout(@CliOption(key = "", mandatory = true) Train train) throws Exception {
@@ -71,6 +77,34 @@ class GitCommands extends TimedCommand {
return StringUtils.collectionToDelimitedString(git.getTags(project).asList(), "\n");
}
@CliCommand("git previous")
public String previous(@CliOption(key = "", mandatory = true) TrainIteration iteration) throws Exception {
return git.getPreviousIteration(iteration).toString();
}
@CliCommand("git changelog")
public String changelog(@CliOption(key = "", mandatory = true) TrainIteration iteration) throws Exception {
TrainIteration previousIteration = git.getPreviousIteration(iteration);
return ExecutionUtils.runAndReturn(executor, iteration, moduleIteration -> {
List<TicketReference> ticketRefs = git.getTicketReferencesBetween(moduleIteration.getProject(), previousIteration,
iteration);
return Changelog.of(moduleIteration, toTickets(ticketRefs));
}).stream() //
.map(Changelog::toString) //
.collect(Collectors.joining("\n"));
}
private Tickets toTickets(List<TicketReference> ticketRefs) {
return new Tickets(
ticketRefs.stream().map(it -> new Ticket(it.getId(), it.getMessage(), "", null)).collect(Collectors.toList()));
}
/**
* Resets all projects contained in the given {@link Train}.
*

View File

@@ -22,18 +22,12 @@ import lombok.experimental.FieldDefaults;
import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.TimeZone;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.eclipse.jgit.api.CheckoutCommand;
import org.eclipse.jgit.api.CherryPickResult;
@@ -63,15 +57,19 @@ import org.eclipse.jgit.transport.URIish;
import org.springframework.data.release.io.Workspace;
import org.springframework.data.release.issues.IssueTracker;
import org.springframework.data.release.issues.Ticket;
import org.springframework.data.release.issues.TicketReference;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.Gpg;
import org.springframework.data.release.model.Iteration;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.ReleaseTrains;
import org.springframework.data.release.model.Train;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.ExecutionUtils;
import org.springframework.data.release.utils.Logger;
import org.springframework.data.util.Pair;
import org.springframework.data.util.Streamable;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.stereotype.Component;
@@ -311,10 +309,40 @@ public class GitOperations {
logger.log(project, "Project update done!");
}
/**
* Updates the given {@link Project} by fetching all tags.
*
* @param project must not be {@literal null}.
*/
public void fetchTags(Project project) {
Assert.notNull(project, "Project must not be null!");
logger.log(project, "Updating project tags…");
GitProject gitProject = new GitProject(project, server);
String repositoryName = gitProject.getRepositoryName();
doWithGit(project, git -> {
if (workspace.hasProjectDirectory(project)) {
logger.log(project, "Found existing repository %s. Obtaining tags…", repositoryName);
logger.log(project, "git fetch --tags");
git.fetch().setTagOpt(TagOpt.FETCH_TAGS).call();
} else {
clone(project);
}
});
logger.log(project, "Project tags update done!");
}
public VersionTags getTags(Project project) {
return doWithGit(project, git -> {
return new VersionTags(git.tagList().call().stream()//
return new VersionTags(project, git.tagList().call().stream()//
.map(ref -> {
RevCommit commit = getCommit(git.getRepository(), ref);
@@ -375,6 +403,120 @@ public class GitOperations {
});
}
/**
* Lookup the previous {@link TrainIteration} from existing tags.
*
* @param trainIteration must not be {@literal null}.
* @return
* @throws IllegalStateException if no previous iteration could be found.
*/
public TrainIteration getPreviousIteration(TrainIteration trainIteration) {
Assert.notNull(trainIteration, "TrainIteration must not be null!");
if (trainIteration.getIteration().isMilestone() && trainIteration.getIteration().getIterationValue() == 1) {
Train trainToUse = getPreviousTrain(trainIteration);
return trainToUse.getIteration(Iteration.GA);
}
Optional<TrainIteration> mostRecentBefore = getTags(Projects.BUILD) //
.filter((tag, ti) -> ti.getTrain().equals(trainIteration.getTrain())) //
.find((tag, iteration) -> iteration.getIteration().compareTo(trainIteration.getIteration()) < 0,
Pair::getSecond);
return mostRecentBefore.orElseThrow(() -> new IllegalStateException(
"Cannot determine previous iteration for " + trainIteration.getReleaseTrainNameAndVersion()));
}
public List<TicketReference> getTicketReferencesBetween(Project project, TrainIteration from, TrainIteration to) {
VersionTags tags = getTags(project);
List<TicketReference> ticketReferences = doWithGit(project, git -> {
Repository repo = git.getRepository();
ModuleIteration toModuleIteration = to.getModule(project);
ObjectId fromTag = resolveLowerBoundary(project, from, tags, repo);
ObjectId toTag = resolveUpperBoundary(toModuleIteration, tags, repo);
Iterable<RevCommit> commits = git.log().addRange(fromTag, toTag).call();
return StreamSupport.stream(commits.spliterator(), false).flatMap(it -> {
ParsedCommitMessage message = ParsedCommitMessage.parse(it.getFullMessage());
if (message.getTicketReference() == null) {
logger.warn(toModuleIteration, "Commit %s does not refer to a ticket (%s)", it.getName(),
it.getShortMessage());
return Stream.empty();
}
return Stream.of(message.getTicketReference());
}).collect(Collectors.toList());
});
// make TicketReference unique
Set<String> uniqueIds = new HashSet<>();
List<TicketReference> uniqueTicketReferences = new ArrayList<>();
for (TicketReference reference : ticketReferences) {
if (uniqueIds.add(reference.getId())) {
uniqueTicketReferences.add(reference);
}
}
uniqueTicketReferences.sort(Comparator.<TicketReference> naturalOrder().reversed());
return uniqueTicketReferences;
}
protected ObjectId resolveLowerBoundary(Project project, TrainIteration iteration, VersionTags tags, Repository repo)
throws IOException {
if (iteration.contains(project)) {
Optional<Tag> fromTag = tags.filter(iteration.getTrain()).findTag(iteration.getIteration());
Tag tag = fromTag.get();
return repo.parseCommit(repo.resolve(tag.getName()));
}
return repo.resolve(getFirstCommit(repo));
}
protected ObjectId resolveUpperBoundary(ModuleIteration iteration, VersionTags tags, Repository repo)
throws IOException {
Optional<Tag> tag = tags.filter(iteration.getTrain()).findTag(iteration.getIteration());
String rangeEnd = tag.map(Tag::getName).orElse(Branch.from(iteration).toString());
return repo.parseCommit(repo.resolve(rangeEnd));
}
private static String getFirstCommit(Repository repo) throws IOException {
try (RevWalk revWalk = new RevWalk(repo)) {
return revWalk.parseCommit(repo.resolve("master")).getName();
}
}
private static Train getPreviousTrain(TrainIteration trainIteration) {
Train trainToUse = ReleaseTrains.CODD;
for (Train train : ReleaseTrains.trains()) {
if (train.isBefore(trainIteration.getTrain())) {
trainToUse = train;
} else {
break;
}
}
return trainToUse;
}
private Stream<Branch> getRemoteBranches(Project project) {
return doWithGit(project, git -> {
@@ -901,4 +1043,9 @@ public class GitOperations {
return false;
}
}
private static class VersionedIterations {
}
}

View File

@@ -0,0 +1,196 @@
/*
* Copyright 2020 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 lombok.Getter;
import lombok.ToString;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.data.release.issues.TicketReference;
/**
* @author Mark Paluch
*/
@Getter
@ToString
class ParsedCommitMessage {
private static final Pattern JIRA_TICKET = Pattern.compile("(?>\\[)?([A-Z]+[ ]?-[ ]?\\d+)(?>\\])?");
private static final Pattern GITHUB_TICKET = Pattern.compile("((?>#|gh-)\\d+)");
private static final Pattern GITHUB_CLOSE_SYNTAX = Pattern.compile(
"(?>closes|closed|close|fixes|fixed|fix|resolves|resolved|resolve|see)[\\s:]*((?>#|gh-)\\d+)",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
private static final Pattern GITHUB_PREFIX_SYNTAX = Pattern.compile("^(#\\d+)");
private static final Pattern A_TICKET = Pattern
.compile(String.format("(%s|%s)", JIRA_TICKET.pattern(), GITHUB_TICKET.pattern()));
private static final Pattern ORIGINAL_PULL_REQUEST = Pattern
.compile("Original (?>pull request|PR|pullrequest):(?>\\s+)?" + A_TICKET.pattern(), Pattern.CASE_INSENSITIVE);
private static final Pattern RELATED_TICKET = Pattern.compile(
"Related (?>tickets|ticket):(?>\\s+)?((" + A_TICKET.pattern() + "(?>[\\s,]*))+)", Pattern.CASE_INSENSITIVE);
private final String summary;
private final String body;
private final TicketReference ticketReference;
private final TicketReference pullRequestReference;
private final List<TicketReference> relatedTickets;
private ParsedCommitMessage(String summary, String body, TicketReference ticketReference,
TicketReference pullRequestReference, List<TicketReference> relatedTickets) {
this.summary = summary;
this.body = body;
this.ticketReference = ticketReference;
this.pullRequestReference = pullRequestReference;
this.relatedTickets = relatedTickets;
}
public static ParsedCommitMessage parse(String message) {
int lineBreak = message.indexOf('\n');
String summary = null;
String body = null;
TicketReference ticketReference = null;
TicketReference pullRequestReference = null;
if (lineBreak > -1) {
summary = message.substring(0, lineBreak).trim();
body = message.substring(lineBreak + 1).trim();
} else {
summary = message.trim();
}
// DATACASS-nnn - syntax
Matcher jiraMatcher = JIRA_TICKET.matcher(summary);
if (jiraMatcher.find()) {
MatchResult mr = jiraMatcher.toMatchResult();
// allow […] syntax and start of message syntax
if (mr.start(1) < 2) {
int summaryStart = findSummaryIndex(summary, mr.end(1));
ticketReference = new TicketReference(jiraMatcher.group(1).toUpperCase(Locale.ROOT),
summaryStart > -1 ? summary.substring(summaryStart) : summary, TicketReference.Style.Jira);
}
}
// Closes (gh-nnn|#nnn) syntax
Matcher gitHubMatcher = GITHUB_CLOSE_SYNTAX.matcher(message);
// #nnn syntax
Matcher gitHubPrefixMatcher = GITHUB_PREFIX_SYNTAX.matcher(summary);
if (gitHubPrefixMatcher.find()) {
MatchResult mr = gitHubPrefixMatcher.toMatchResult();
if (mr.start(1) == 0) {
int summaryStart = findSummaryIndex(summary, mr.end(1));
ticketReference = new TicketReference(gitHubPrefixMatcher.group(1).toUpperCase(Locale.ROOT),
summaryStart > -1 ? summary.substring(summaryStart) : summary, TicketReference.Style.GitHub);
}
} else {
if (gitHubMatcher.find()) {
ticketReference = new TicketReference(gitHubMatcher.group(1), summary, TicketReference.Style.GitHub);
}
}
List<TicketReference> relatedTickets = new ArrayList<>();
if (body != null) {
Matcher relatedTicketsMatcher = RELATED_TICKET.matcher(body);
if (relatedTicketsMatcher.find()) {
String ticketIds[] = relatedTicketsMatcher.group(1).split(",");
for (String ticketId : ticketIds) {
extractTicket(ticketId).ifPresent(relatedTickets::add);
}
}
while (gitHubMatcher.find()) {
extractTicket(gitHubMatcher.group(1)).ifPresent(relatedTickets::add);
}
}
if (body != null) {
Matcher prMatcher = ORIGINAL_PULL_REQUEST.matcher(body);
if (prMatcher.find()) {
Optional<TicketReference> pullRequest = extractTicket(prMatcher.group(1));
if (pullRequest.isPresent()) {
pullRequestReference = pullRequest.get();
}
if (ticketReference == null && pullRequestReference != null) {
ticketReference = pullRequestReference;
pullRequestReference = null;
}
}
}
return new ParsedCommitMessage(summary, body, ticketReference, pullRequestReference, relatedTickets);
}
protected static Optional<TicketReference> extractTicket(String ticketId) {
if (JIRA_TICKET.matcher(ticketId.trim()).matches()) {
return Optional.of(new TicketReference(ticketId.trim(), null, TicketReference.Style.Jira));
}
if (GITHUB_TICKET.matcher(ticketId.trim()).matches()) {
return Optional.of(new TicketReference(ticketId.trim(), null, TicketReference.Style.GitHub));
}
return Optional.empty();
}
private static int findSummaryIndex(String summary, int startAt) {
int dash = summary.indexOf("- ", startAt);
if (dash > -1) {
return dash + 2;
}
int space = summary.indexOf(" ", startAt);
if (space > -1) {
return space + 1;
}
return -1;
}
}

View File

@@ -67,12 +67,16 @@ public class Tag implements Comparable<Tag> {
public Optional<ArtifactVersion> toArtifactVersion() {
try {
return Optional.of(ArtifactVersion.of(getVersionSource()));
return Optional.of(getRequiredArtifactVersion());
} catch (IllegalArgumentException o_O) {
return Optional.empty();
}
}
public ArtifactVersion getRequiredArtifactVersion() {
return ArtifactVersion.of(getVersionSource());
}
/**
* Creates a new {@link Tag} for the given {@link ArtifactVersion} based on the format of the current one.
*

View File

@@ -17,13 +17,26 @@ package org.springframework.data.release.git;
import lombok.EqualsAndHashCode;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.Iteration;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.ReleaseTrains;
import org.springframework.data.release.model.Train;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.util.Pair;
import org.springframework.data.util.Streamable;
import org.springframework.util.Assert;
@@ -31,21 +44,26 @@ import org.springframework.util.Assert;
* Value object to represent a collection of {@link Tag}s.
*
* @author Oliver Gierke
* @author Mark Paluch
*/
@EqualsAndHashCode
public class VersionTags implements Streamable<Tag> {
private final Project project;
private final List<Tag> tags;
/**
* Creates a new {@link VersionTags} instance for the given {@link List} of {@link Tag}s.
* Creates a new {@link VersionTags} instance for the given {@link Project} and {@link List} of {@link Tag}s.
*
* @param project must not be {@literal null}.
* @param source must not be {@literal null}.
*/
VersionTags(List<Tag> source) {
VersionTags(Project project, Collection<Tag> source) {
Assert.notNull(project, "Project must not be null!");
Assert.notNull(source, "Tags must not be null!");
this.project = project;
this.tags = source.stream().//
filter(Tag::isVersionTag).//
sorted().collect(Collectors.toList());
@@ -93,4 +111,140 @@ public class VersionTags implements Streamable<Tag> {
public Iterator<Tag> iterator() {
return tags.iterator();
}
/**
* Create {@link VersionTags} that contains only tags from the given {@link Train}.
*
* @param train
* @return
*/
public VersionTags filter(Train train) {
return filter((tag, ti) -> ti.getTrain().equals(train));
}
/**
* Create {@link VersionTags} from a filtered subset of this {@link VersionTags}.
*
* @param predicate
* @return
*/
public VersionTags filter(BiPredicate<Tag, TrainIteration> predicate) {
Map<Tag, TrainIteration> iterations = newMap();
withIterations().forEach((tag, trainIteration) -> {
if (predicate.test(tag, trainIteration)) {
iterations.put(tag, trainIteration);
}
});
return new VersionTags(project, iterations.keySet());
}
Map<Tag, TrainIteration> withIterations() {
Map<Tag, TrainIteration> iterations = newMap();
for (Tag tag : tags) {
Optional<ArtifactVersion> artifactVersion = tag.toArtifactVersion();
if (!artifactVersion.isPresent()) {
continue;
}
ArtifactVersion version = artifactVersion.get();
Optional<Iteration> iteration = toIteration(version);
if (!iteration.isPresent()) {
continue;
}
Optional<TrainIteration> ti = getTrainIteration(version, project, iteration.get());
ti.ifPresent(it -> iterations.put(tag, it));
}
return iterations;
}
public Optional<TrainIteration> findMostRecentTrainIterationBefore(Iteration iterationToFind) {
return find((tag, iteration) -> iteration.getIteration().compareTo(iterationToFind) < 0, Pair::getSecond);
}
public Optional<Tag> findTag(Iteration iterationToFind) {
return find((tag, iteration) -> iteration.getIteration().compareTo(iterationToFind) == 0, Pair::getFirst);
}
public <T> Optional<T> find(BiPredicate<Tag, TrainIteration> filter,
Function<Pair<Tag, TrainIteration>, T> resultExtractor) {
for (Map.Entry<Tag, TrainIteration> entry : withIterations().entrySet()) {
if (filter.test(entry.getKey(), entry.getValue())) {
return Optional.of(resultExtractor.apply(Pair.of(entry.getKey(), entry.getValue())));
}
}
return Optional.empty();
}
private static <K, V> TreeMap<K, V> newMap() {
return new TreeMap(Comparator.reverseOrder());
}
private static Optional<TrainIteration> getTrainIteration(ArtifactVersion version, Project project,
Iteration iteration) {
// consider major version bump during ReleaseTrain
boolean minorIncrementedButTargetsNextTrain = false;
for (Train train : ReleaseTrains.trains()) {
TrainIteration ti = new TrainIteration(train, iteration);
if (!ti.contains(project)) {
continue;
}
if (minorIncrementedButTargetsNextTrain) {
return Optional.of(ti);
}
ModuleIteration mi = ti.getModule(project);
ArtifactVersion artifactVersion = ArtifactVersion.of(mi);
if (artifactVersion.equals(version)) {
return Optional.of(ti);
}
if (artifactVersion.getNextMinorVersion().equals(version)) {
minorIncrementedButTargetsNextTrain = true;
}
}
return Optional.empty();
}
private static Optional<Iteration> toIteration(ArtifactVersion version) {
if (version.isMilestoneVersion()) {
return Optional.of(Iteration.valueOf("M" + version.getLevel()));
}
if (version.isReleaseCandidateVersion()) {
return Optional.of(Iteration.valueOf("RC" + version.getLevel()));
}
if (version.isBugFixVersion()) {
return Optional.of(Iteration.valueOf("SR" + version.getLevel()));
}
if (version.isReleaseVersion()) {
return Optional.of(Iteration.GA);
}
return Optional.empty();
}
}

View File

@@ -61,8 +61,11 @@ public class Changelog {
String summary = ticket.getSummary();
builder.append("* ").append(ticket.getId()).append(" - ").append(summary.trim());
builder.append("* ").append(ticket.getId()).append(" - ").append(summary != null ? summary.trim() : "");
if (summary == null) {
System.out.println();
}
if (!summary.endsWith(".")) {
builder.append(".");
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2020 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.issues;
import lombok.Value;
/**
* @author Mark Paluch
*/
@Value
public class TicketReference implements Comparable<TicketReference> {
String id;
String message;
Style style;
public TicketReference(String id, String message, Style style) {
this.id = normalize(id);
this.message = message;
this.style = style;
}
private static String normalize(String id) {
if (id.toLowerCase().startsWith("gh-")) {
return "#" + id.substring(3);
}
return id.toUpperCase().replaceAll(" ", "");
}
@Override
public int compareTo(TicketReference o) {
if (id.startsWith("#") && o.id.startsWith("#")) {
return Integer.compare(Integer.parseInt(id.substring(1)), Integer.parseInt(o.id.substring(1)));
}
return id.compareToIgnoreCase(o.id);
}
public enum Style {
GitHub, Jira;
}
}

View File

@@ -15,10 +15,10 @@
*/
package org.springframework.data.release.model;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.With;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -30,7 +30,6 @@ import org.springframework.util.Assert;
* @author Oliver Gierke
* @author Mark Paluch
*/
@EqualsAndHashCode
public class ArtifactVersion implements Comparable<ArtifactVersion> {
private static final Pattern PATTERN = Pattern
@@ -40,12 +39,13 @@ public class ArtifactVersion implements Comparable<ArtifactVersion> {
.compile("((\\d+)\\.(\\d+)(\\.\\d+)?)(-((RC\\d+)|(M\\d+)|(SNAPSHOT)))?");
private static final String RELEASE_SUFFIX = "RELEASE";
private static final String MILESTONE_SUFFIX = "M\\d|RC\\d";
private static final String MILESTONE_SUFFIX = "M\\d";
private static final String RC_SUFFIX = "RC\\d";
private static final String SNAPSHOT_SUFFIX = "BUILD-SNAPSHOT";
private static final String SNAPSHOT_MODIFIER = "SNAPSHOT";
private static final String VALID_SUFFIX = String.format("%s|%s|%s|-%s|-%s|-%s", RELEASE_SUFFIX, MILESTONE_SUFFIX,
SNAPSHOT_SUFFIX, RELEASE_SUFFIX, MILESTONE_SUFFIX, SNAPSHOT_MODIFIER);
private static final String VALID_SUFFIX = String.format("%s|%s|%s|%s|-%s|-%s|-%s", RELEASE_SUFFIX, MILESTONE_SUFFIX,
RC_SUFFIX, SNAPSHOT_SUFFIX, RELEASE_SUFFIX, MILESTONE_SUFFIX, SNAPSHOT_MODIFIER);
private final @Getter Version version;
private final @With boolean modifierFormat;
@@ -175,6 +175,39 @@ public class ArtifactVersion implements Comparable<ArtifactVersion> {
return suffix.matches(MILESTONE_SUFFIX);
}
/**
* Returns whether the version is a RC version.
*
* @return
*/
public boolean isReleaseCandidateVersion() {
return suffix.matches(RC_SUFFIX);
}
public int getLevel() {
if (isMilestoneVersion()) {
Pattern pattern = Pattern.compile("M(\\d+)");
Matcher matcher = pattern.matcher(suffix);
matcher.find();
return Integer.parseInt(matcher.group(1));
}
if (isReleaseCandidateVersion()) {
Pattern pattern = Pattern.compile("RC(\\d+)");
Matcher matcher = pattern.matcher(suffix);
matcher.find();
return Integer.parseInt(matcher.group(1));
}
if (isBugFixVersion()) {
return version.getBugfix();
}
throw new IllegalStateException("Not a M/RC/SR release");
}
public boolean isSnapshotVersion() {
return suffix.matches(SNAPSHOT_SUFFIX) || suffix.matches(SNAPSHOT_MODIFIER);
}
@@ -217,9 +250,16 @@ public class ArtifactVersion implements Comparable<ArtifactVersion> {
return isSnapshotVersion() ? this : new ArtifactVersion(version, modifierFormat, getSnapshotSuffix());
}
/**
* @return the next minor version retaining the modifier and snapshot suffix.
*/
public ArtifactVersion getNextMinorVersion() {
return new ArtifactVersion(version.nextMinor(), modifierFormat, suffix);
}
public String getReleaseTrainSuffix() {
if (isSnapshotVersion() || isMilestoneVersion()) {
if (isSnapshotVersion() || isMilestoneVersion() || isReleaseCandidateVersion()) {
return suffix;
}
@@ -250,7 +290,7 @@ public class ArtifactVersion implements Comparable<ArtifactVersion> {
if (modifierFormat) {
if (isSnapshotVersion() || isMilestoneVersion()) {
if (isSnapshotVersion() || isMilestoneVersion() || isReleaseCandidateVersion()) {
return String.format("%s-%s", version.toMajorMinorBugfix(), suffix);
}
@@ -276,4 +316,24 @@ public class ArtifactVersion implements Comparable<ArtifactVersion> {
public String getGeneration() {
return String.format("%s.%s.x", version.getMajor(), version.getMinor());
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ArtifactVersion)) {
return false;
}
ArtifactVersion other = (ArtifactVersion) o;
return version.equals(other.version) && isReleaseVersion() == other.isReleaseVersion()
&& isSnapshotVersion() == other.isSnapshotVersion() && isMilestoneVersion() == other.isMilestoneVersion()
&& isReleaseCandidateVersion() == other.isReleaseCandidateVersion();
}
@Override
public int hashCode() {
return Objects.hash(version, isReleaseVersion(), isSnapshotVersion(), isMilestoneVersion(),
isReleaseCandidateVersion());
}
}

View File

@@ -139,4 +139,8 @@ public class ReleaseTrains {
.findFirst() //
.orElse(null);
}
public static List<Train> trains() {
return TRAINS;
}
}

View File

@@ -46,9 +46,10 @@ import org.springframework.util.Assert;
*/
@Value
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode(of = "name")
public class Train implements Streamable<Module> {
private final String name;;
private final String name;
private final Modules modules;
private @Nullable Version calver;
private @With Iterations iterations;
@@ -136,7 +137,7 @@ public class Train implements Streamable<Module> {
Module module = getModule(project);
return new ModuleIteration(module, getIteration(iteration));
return new ModuleIteration(module, doGetTrainIteration(iteration));
}
public Iterable<ModuleIteration> getModuleIterations(Iteration iteration) {
@@ -154,7 +155,7 @@ public class Train implements Streamable<Module> {
}
public TrainIteration getIteration(String name) {
return new TrainIteration(this, iterations.getIterationByName(name));
return doGetTrainIteration(iterations.getIterationByName(name));
}
@@ -184,6 +185,31 @@ public class Train implements Streamable<Module> {
return new Train(name, Modules.of(modules), calver, iterations, alwaysUseBranch);
}
/**
* Check whether this train comes before {@link Train other}.
*
* @param other
* @return
*/
public boolean isBefore(Train other) {
Assert.notNull(other, "Train must not be null");
if (usesCalver() && other.usesCalver()) {
return getCalver().compareTo(other.getCalver()) < 0;
}
if (usesCalver()) {
return false;
}
if (other.usesCalver()) {
return true;
}
return getName().compareToIgnoreCase(other.getName()) < 0;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
@@ -216,6 +242,10 @@ public class Train implements Streamable<Module> {
Assert.isTrue(iterations.contains(iteration),
String.format("Iteration %s is not a valid one for the configured iterations %s!", iteration, iterations));
return doGetTrainIteration(iteration);
}
protected TrainIteration doGetTrainIteration(Iteration iteration) {
return new TrainIteration(this, iteration);
}

View File

@@ -23,17 +23,24 @@ import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.release.AbstractIntegrationTests;
import org.springframework.data.release.model.Iteration;
import org.springframework.data.release.model.ReleaseTrains;
import org.springframework.data.release.model.TestReleaseTrains;
import org.springframework.data.release.model.TrainIteration;
/**
* @author Oliver Gierke
* @author Mark Paluch
*/
@Disabled
class GitOperationsIntegrationTests extends AbstractIntegrationTests {
@@ -75,4 +82,49 @@ class GitOperationsIntegrationTests extends AbstractIntegrationTests {
void obtainsVersionTagsForRepoThatAlsoHasOtherTags() {
gitOperations.getTags(MONGO_DB);
}
@Test
void shouldDeterminePreviousIterationFromGA() throws Exception {
TrainIteration hopperRc1 = gitOperations.getPreviousIteration(ReleaseTrains.HOPPER.getIteration(Iteration.GA));
assertThat(hopperRc1.getTrain()).isEqualTo(ReleaseTrains.HOPPER);
assertThat(hopperRc1.getIteration()).isEqualTo(Iteration.RC1);
TrainIteration hopperM1 = gitOperations.getPreviousIteration(ReleaseTrains.HOPPER.getIteration(Iteration.RC1));
assertThat(hopperM1.getTrain()).isEqualTo(ReleaseTrains.HOPPER);
assertThat(hopperM1.getIteration()).isEqualTo(Iteration.M1);
TrainIteration hopperSR9 = gitOperations.getPreviousIteration(ReleaseTrains.HOPPER.getIteration(Iteration.SR10));
assertThat(hopperSR9.getTrain()).isEqualTo(ReleaseTrains.HOPPER);
assertThat(hopperSR9.getIteration()).isEqualTo(Iteration.SR9);
}
@Test
void verify() throws Exception {
// gitOperations.update(ReleaseTrains.HOPPER);
try (Git git = new Git(new FileRepository("/Users/mpaluch/git/data/spring-data-elasticsearch/.git"))) {
ObjectId resolve = git.getRepository().parseCommit(git.getRepository().resolve("3.0.0.RELEASE"));
Iterable<RevCommit> master = git.log().addRange(resolve, git.getRepository().resolve("master")).call();
for (RevCommit revCommit : master) {
ParsedCommitMessage message = ParsedCommitMessage.parse(revCommit.getFullMessage());
if (message.getTicketReference() == null || message.getSummary() == null) {
System.out.println(revCommit.getFullMessage());
System.out.println();
}
}
} catch (Exception o_O) {
throw new RuntimeException(o_O);
}
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2020 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 static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
/**
* Unit tests for {@link ParsedCommitMessage}.
*
* @author Mark Paluch
*/
class ParsedCommitMessageUnitTests {
@Test
void shouldParsePlainCommit() {
ParsedCommitMessage commit = ParsedCommitMessage.parse("hello world");
assertThat(commit.getSummary()).isEqualTo("hello world");
assertThat(commit.getBody()).isNull();
}
@ParameterizedTest
@ValueSource(strings = { "DATAFOO-123 - Hello World.", "DATAFOO-123 Hello World.", "[DATAFOO-123] - Hello World.",
"DATAFOO - 123 - Hello World." })
void shouldParseOneLinerCommitWithJiraTicket(String commitMessage) {
ParsedCommitMessage commit = ParsedCommitMessage.parse(commitMessage);
assertThat(commit.getSummary()).endsWith("Hello World.");
assertThat(commit.getTicketReference().getId()).isEqualTo("DATAFOO-123");
}
@Test
void considersMultipleTicketsAsRelatedTickets() {
ParsedCommitMessage commit = ParsedCommitMessage
.parse("Polishing\n\nAdd tests for write and delete.\n\nCloses gh-503\nCloses gh-511");
assertThat(commit.getTicketReference().getId()).isEqualTo("#503");
assertThat(commit.getRelatedTickets()).hasSize(1);
assertThat(commit.getRelatedTickets().get(0).getId()).isEqualTo("#511");
}
@ParameterizedTest
@ValueSource(strings = { "close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved" })
void considersResolvesSyntax(String prefix) {
ParsedCommitMessage commit = ParsedCommitMessage.parse("Polishing\n\n" + prefix + " gh-586.");
assertThat(commit.getTicketReference().getId()).isEqualTo("#586");
}
@ParameterizedTest
@ValueSource(strings = { "Incorporate review feedback\n\nSee gh-574." })
void shouldParseCommitWithSeeTicket(String commitMessage) {
ParsedCommitMessage commit = ParsedCommitMessage.parse(commitMessage);
assertThat(commit.getSummary()).isEqualTo("Incorporate review feedback");
assertThat(commit.getTicketReference().getId()).isEqualTo("#574");
}
@Test
void shouldParseCommitWithRelatedTickets() {
ParsedCommitMessage commit = ParsedCommitMessage
.parse("DATAFOO-123 - Hello World.\n Related tickets: DATACMNS-1438, DATACMNS-1461, DATACMNS-1609.");
assertThat(commit.getTicketReference().getId()).isEqualTo("DATAFOO-123");
assertThat(commit.getRelatedTickets()).hasSize(3);
}
@ParameterizedTest
@ValueSource(strings = { "DATAFOO-456 - Hello World.\n Original pull request: #415.",
"DATAFOO-456 - Hello World.\n Original pr: #415." })
void shouldParseCommitWithPullRequest(String commitMessage) {
ParsedCommitMessage commit = ParsedCommitMessage.parse(commitMessage);
assertThat(commit.getTicketReference().getId()).isEqualTo("DATAFOO-456");
assertThat(commit.getPullRequestReference()).isNotNull();
assertThat(commit.getPullRequestReference().getId()).isEqualTo("#415");
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2020 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 static org.assertj.core.api.Assertions.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.springframework.data.release.model.Iteration;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.ReleaseTrains;
/**
* Unit tests for {@link VersionTags}.
*
* @author Mark Paluch
*/
class VersionTagsUnitTests {
@Test
void shouldDetermineTagForIteration() {
VersionTags tags = new VersionTags(Projects.NEO4J,
Stream.of("5.3.0.RELEASE", "5.4.0-M1", "6.0.0-M2").map(Tag::of).collect(Collectors.toList()));
assertThat(tags.filter(ReleaseTrains.NEUMANN).findTag(Iteration.GA))
.hasValueSatisfying(actual -> assertThat(actual.getName()).isEqualTo("5.3.0.RELEASE"));
assertThat(tags.filter(ReleaseTrains.OCKHAM).findTag(Iteration.M2))
.hasValueSatisfying(actual -> assertThat(actual.getName()).isEqualTo("6.0.0-M2"));
}
@Test
void shouldDetermineTagForMajorVersionBumpDuringDevelopment() {
VersionTags tags = new VersionTags(Projects.NEO4J,
Stream.of("5.3.0.RELEASE", "5.4.0-M1", "6.0.0-M2").map(Tag::of).collect(Collectors.toList()));
assertThat(tags.filter(ReleaseTrains.OCKHAM).findTag(Iteration.M1))
.hasValueSatisfying(actual -> assertThat(actual.getName()).isEqualTo("5.4.0-M1"));
}
}

View File

@@ -47,4 +47,23 @@ class TrainsUnitTest {
assertThat(ReleaseTrains.PASCAL.getModule(Projects.BOM).getVersion().toMajorMinorBugfix()).isEqualTo("2021.0.0");
}
@Test
void beforeShouldConsiderNonCalver() {
assertThat(ReleaseTrains.HOPPER.isBefore(ReleaseTrains.GOSLING)).isFalse();
assertThat(ReleaseTrains.HOPPER.isBefore(ReleaseTrains.INGALLS)).isTrue();
}
@Test
void beforeShouldConsiderCalver() {
assertThat(ReleaseTrains.PASCAL.isBefore(ReleaseTrains.OCKHAM)).isFalse();
assertThat(ReleaseTrains.OCKHAM.isBefore(ReleaseTrains.PASCAL)).isTrue();
}
@Test
void beforeShouldConsiderMixedCalver() {
assertThat(ReleaseTrains.PASCAL.isBefore(ReleaseTrains.INGALLS)).isFalse();
assertThat(ReleaseTrains.INGALLS.isBefore(ReleaseTrains.PASCAL)).isTrue();
}
}