diff --git a/releaser-core/src/main/java/releaser/internal/git/GitRepo.java b/releaser-core/src/main/java/releaser/internal/git/GitRepo.java index 06813923..2ffb7bf7 100644 --- a/releaser-core/src/main/java/releaser/internal/git/GitRepo.java +++ b/releaser-core/src/main/java/releaser/internal/git/GitRepo.java @@ -19,9 +19,13 @@ package releaser.internal.git; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.Optional; +import java.util.stream.Stream; import com.jcraft.jsch.IdentityRepository; import com.jcraft.jsch.JSch; @@ -44,9 +48,12 @@ import org.eclipse.jgit.api.ResetCommand; import org.eclipse.jgit.api.TransportConfigCallback; import org.eclipse.jgit.api.errors.EmptyCommitException; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.FetchResult; import org.eclipse.jgit.transport.JschConfigSessionFactory; @@ -247,11 +254,47 @@ class GitRepo { } } + /** + * List all the tags in the repository. + * @return a {@link List} of all the tags in the repository. + */ + Stream listTags() { + try (Git git = this.gitFactory.open(file(this.basedir))) { + final RevWalk walk = new RevWalk(git.getRepository()); + List allTagsNewestFirst = git.tagList().call(); + Collections.sort(allTagsNewestFirst, (Comparator) (o1, o2) -> { + Date d1; + Date d2; + try { + d1 = walk.parseTag(o1.getObjectId()).getTaggerIdent().getWhen(); + } + catch (IOException ioe) { + return 1; // put at end + } + try { + d2 = walk.parseTag(o2.getObjectId()).getTaggerIdent().getWhen(); + } + catch (IOException ioe) { + return -1; // put ahead + } + return d2.compareTo(d1); // more recent first + }); + return allTagsNewestFirst.stream() + .map(ref -> Repository.shortenRefName(ref.getName())); + } + catch (Exception e) { + throw new IllegalStateException("Unable to fetch git tags", e); + } + } + /** * Look for a tag with the given name, and if not found looks for a branch. */ private Optional findTagOrBranchHeadRevision(Git git, String tagOrBranch) - throws GitAPIException { + throws GitAPIException, IOException { + if (tagOrBranch.equals("HEAD")) { + return Optional.of(git.getRepository().exactRef(Constants.HEAD).getTarget()); + } final Optional tag = git.tagList().call().stream() .filter(ref -> ref.getName().equals("refs/tags/" + tagOrBranch)) .findFirst(); diff --git a/releaser-core/src/main/java/releaser/internal/git/ProjectGitHandler.java b/releaser-core/src/main/java/releaser/internal/git/ProjectGitHandler.java index e0f02955..a3fb97bf 100644 --- a/releaser-core/src/main/java/releaser/internal/git/ProjectGitHandler.java +++ b/releaser-core/src/main/java/releaser/internal/git/ProjectGitHandler.java @@ -23,7 +23,9 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.transport.URIish; @@ -318,4 +320,15 @@ public class ProjectGitHandler implements Closeable { CACHE.clear(); } + /** + * Find the tag names that match a {@link Pattern}. + * @param clonedProject the base dir for the cloned repository + * @param tagPattern the {@link Pattern} to use to filter tag names + * @return a {@link Stream} of the tags whose name match the given {@link Pattern} + */ + public Stream findTagNamesMatching(File clonedProject, Pattern tagPattern) { + return gitRepo(clonedProject).listTags() + .filter(tagName -> tagPattern.matcher(tagName).matches()); + } + } diff --git a/releaser-core/src/main/java/releaser/internal/project/ProjectVersion.java b/releaser-core/src/main/java/releaser/internal/project/ProjectVersion.java index 5ec040d9..bd0d8c87 100644 --- a/releaser-core/src/main/java/releaser/internal/project/ProjectVersion.java +++ b/releaser-core/src/main/java/releaser/internal/project/ProjectVersion.java @@ -22,6 +22,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.regex.Pattern; import org.apache.maven.model.Model; @@ -266,6 +267,109 @@ public class ProjectVersion implements Comparable, Serializable return this.assertVersion().major; } + /** + * Compute the previous patch's version, without any prefix or suffix (ie + * MAJOR.MINOR.PATCH only). If the patch number is already at 0, returns an empty + * {@link Optional} instead. + * @param prefix the prefix to prepend to the tag pattern (eg. if tags are in the form + * vVERSION) + * @param suffix the suffix to append to the tag pattern instead (eg. if tags are in + * the form VERSION.RELEASE). this replaces the suffix in the version if any. + * @return an {@link Optional} valued with the previous MAJOR.MINOR.PATCH, or empty if + * PATCH is already 0 + * @see #computePreviousMinorTagPattern(String,String) + */ + public Optional computePreviousPatchTag(String prefix, String suffix) { + SplitVersion splitVersion = this.assertVersion(); + if (suffix.isEmpty()) { + suffix = splitVersion.suffix; + } + int patch; + try { + patch = Integer.parseInt(splitVersion.patch); + } + catch (NumberFormatException nfe) { + throw new IllegalArgumentException("Version " + this.version + + " doesn't contain a numerical PATCH number", nfe); + } + if (patch == 0) { + return Optional.empty(); + } + return Optional.of(prefix + splitVersion.major + splitVersion.delimiter + + splitVersion.minor + splitVersion.delimiter + (patch - 1) + + splitVersion.delimiter + suffix); + } + + /** + * Compute a {@link Pattern} that allows to identify all the version tags of the + * previous MAJOR.MINOR (MAJOR.MINOR.*), provided MINOR is not 0. If MINOR is 0, + * {@link Optional#empty()} is returned. + * @param prefix the prefix to prepend to the tag pattern (eg. if tags are in the form + * vVERSION) + * @param suffix the suffix to append to the tag pattern (eg. if tags are in the form + * VERSION.RELEASE) + * @return a {@link Optional} of {@link Pattern} to identify all the versions in the + * previous MAJOR.MINOR, or empty if current MINOR is 0 + * @see #computePreviousMajorTagPattern(String, String) + */ + public Optional computePreviousMinorTagPattern(String prefix, + String suffix) { + SplitVersion splitVersion = this.assertVersion(); + if (suffix.isEmpty()) { + suffix = splitVersion.suffix; + } + String quotedSuffix = Pattern.quote(splitVersion.delimiter + suffix); + int minor; + try { + minor = Integer.parseInt(splitVersion.minor); + } + catch (NumberFormatException nfe) { + throw new IllegalArgumentException("Version " + this.version + + " doesn't contain a numerical MINOR number", nfe); + } + if (minor == 0) { + return Optional.empty(); + } + Pattern p = Pattern + .compile(Pattern + .quote(prefix + splitVersion.major + splitVersion.delimiter + + (minor - 1) + splitVersion.delimiter) + + "\\d+" + quotedSuffix); + return Optional.of(p); + } + + /** + * Compute a {@link Pattern} that allows to identify all the version tags of the + * previous MAJOR. Throws {@link IllegalArgumentException} if MAJOR is 0. + * @param prefix the prefix to prepend to the tag pattern (eg. if tags are in the form + * vVERSION) + * @param suffix the suffix to append to the tag pattern (eg. if tags are in the form + * VERSION.RELEASE) + * @return a {@link Pattern} to identify all the versions in the previous MAJOR + */ + public Pattern computePreviousMajorTagPattern(String prefix, String suffix) { + SplitVersion splitVersion = this.assertVersion(); + if (suffix.isEmpty()) { + suffix = splitVersion.suffix; + } + String quotedSuffix = Pattern.quote(splitVersion.delimiter + suffix); + int major; + try { + major = Integer.parseInt(splitVersion.major); + } + catch (NumberFormatException nfe) { + throw new IllegalArgumentException("Version " + this.version + + " doesn't contain a numerical MAJOR number", nfe); + } + if (major == 0) { + throw new IllegalArgumentException( + "Cannot compute previous MAJOR pattern with MAJOR of 0"); + } + return Pattern.compile( + Pattern.quote(prefix + (major - 1) + splitVersion.delimiter) + "\\d+" + + Pattern.quote(splitVersion.delimiter) + "\\d+" + quotedSuffix); + } + public boolean isSnapshot() { return this.version != null && this.version.contains("SNAPSHOT"); } diff --git a/releaser-core/src/test/java/releaser/internal/buildsystem/ProjectVersionTests.java b/releaser-core/src/test/java/releaser/internal/buildsystem/ProjectVersionTests.java index b803560a..aff8d7d3 100644 --- a/releaser-core/src/test/java/releaser/internal/buildsystem/ProjectVersionTests.java +++ b/releaser-core/src/test/java/releaser/internal/buildsystem/ProjectVersionTests.java @@ -27,6 +27,7 @@ import org.junit.Test; import releaser.internal.git.GitRepoTests; import releaser.internal.project.ProjectVersion; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.BDDAssertions.then; import static org.assertj.core.api.BDDAssertions.thenThrownBy; @@ -620,6 +621,69 @@ public class ProjectVersionTests { then(projectVersion("1.0.0.BUILD-SNAPSHOT").releaseTagName()).isEmpty(); } + @Test + public void should_return_decremented_patch_when_patch_above_zero() { + then(projectVersion("1.2.3.RELEASE").computePreviousPatchTag("v", "RELEASE")) + .contains("v1.2.2.RELEASE"); + } + + @Test + public void should_use_current_suffix_if_no_forced_suffix() { + then(projectVersion("1.2.3.WHATEVER").computePreviousPatchTag("v", "")) + .contains("v1.2.2.WHATEVER"); + } + + @Test + public void should_replace_suffix() { + then(projectVersion("1.2.3.WHATEVER").computePreviousPatchTag("v", "RELEASE")) + .contains("v1.2.2.RELEASE"); + } + + @Test + public void should_return_empty_when_patch_at_zero() { + then(projectVersion("1.2.0.WHATEVER").computePreviousPatchTag("v", "RELEASE")) + .isEmpty(); + } + + @Test + public void should_return_minor_pattern_when_minor_above_zero() { + then(projectVersion("1.2.0.WHATEVER") + .computePreviousMinorTagPattern("v", "RELEASE").get().pattern()) + .isEqualTo("\\Qv1.1.\\E\\d+\\Q.RELEASE\\E"); + } + + @Test + public void should_compute_minor_pattern_with_current_suffix_if_no_forced_suffix() { + then(projectVersion("1.2.0.WHATEVER").computePreviousMinorTagPattern("v", "") + .get().pattern()).isEqualTo("\\Qv1.1.\\E\\d+\\Q.WHATEVER\\E"); + } + + @Test + public void should_return_empty_minor_pattern_when_minor_zero() { + then(projectVersion("1.0.0.WHATEVER").computePreviousMinorTagPattern("v", + "RELEASE")).isEmpty(); + } + + @Test + public void should_return_major_pattern_when_major_above_zero() { + then(projectVersion("1.0.0.WHATEVER") + .computePreviousMajorTagPattern("v", "RELEASE").pattern()) + .isEqualTo("\\Qv0.\\E\\d+\\Q.\\E\\d+\\Q.RELEASE\\E"); + } + + @Test + public void should_compute_major_pattern_with_current_suffix_if_no_forced_suffix() { + then(projectVersion("1.0.0.WHATEVER").computePreviousMajorTagPattern("v", "") + .pattern()).isEqualTo("\\Qv0.\\E\\d+\\Q.\\E\\d+\\Q.WHATEVER\\E"); + } + + @Test + public void should_throw_major_pattern_when_major_zero() { + assertThatIllegalArgumentException() + .isThrownBy(() -> projectVersion("0.0.1.WHATEVER") + .computePreviousMajorTagPattern("v", "RELEASE")); + } + private void thenPatternsForSnapshotMilestoneAndReleaseCandidateArePresent( List unknownTypeOfVersion) { then(unknownTypeOfVersion).isNotEmpty();