diff --git a/README.md b/README.md index 9d156d6f..539a99a5 100644 --- a/README.md +++ b/README.md @@ -51,12 +51,14 @@ Reckon is two things: - an API to infer your next version from a Git repository - applications of that API in various tools (initially, just Gradle) -### Stage Version Scheme +Two schemes are provided to manage pre-release information. + +- _Stages_ for a more structured approach which is a subset of [SemVer](http://semver.org). +- _Snapshots_ for the classic Maven approach to pre-release versions. -Reckon uses an opinionated subset of [SemVer](http://semver.org), meant to provide more structure around how the -pre-release versions are managed. +### Stage Version Scheme -There are three types of versions: +There are three types of stages: | Type | Scheme | Example | Description | |-------------------|----------------------------------------------------------|---------------------------------------------------------|-------------| @@ -85,10 +87,6 @@ Reckon can alternately use SNAPSHOT versions instead of the stage concept. | **final** | `..` | `1.2.3` | A version ready for end-user consumption | | **snapshot** | `..-SNAPSHOT` | `1.3.0-SNAPSHOT` | An intermediate version before the final release is ready. | -### More Information - -See [How Reckon Works](docs/index.md), which includes examples of how reckon will behave in various scenarios. - ## How do I use it? **NOTE:** Check the [Release Notes](https://github.com/ajoberstar/reckon/releases) for details on compatibility and changes. @@ -99,15 +97,14 @@ See [How Reckon Works](docs/index.md), which includes examples of how reckon wil ```groovy plugins { - id 'org.ajoberstar.grgit' version '' // this is a required dependency unless you plan to implement your own VcsInventorySupplier id 'org.ajoberstar.reckon' version '' } reckon { - normal = scopeFromProp() - preRelease = stageFromProp('milestone', 'rc', 'final') - // alternately - // preRelease = snapshotFromProp() + scopeFromProp() + stageFromProp('milestone', 'rc', 'final') + // alternative to stageFromProp + // snapshotFromProp() } ``` @@ -119,7 +116,7 @@ Execute Gradle providing the properties, as needed: - `reckon.stage` - (if you used `stageFromProp`) one of the values passed to `stageFromProp` (defaults to the first alphabetically) to specify what phase of development you are in - (if you used `snapshotFromProp`) either `snapshot` or `final` (defaults to `snapshot`) to specify what phase of development you are in -- `reckon.snapshot` - (**deprecated**, if you used `snapshotFromProp`) one of `true` or `false` (defaults to `true`) to determine whether a snapshot should be made +- `reckon.snapshot` - **deprecated** (if you used `snapshotFromProp`) one of `true` or `false` (defaults to `true`) to determine whether a snapshot should be made When Gradle executes, the version will be inferred as soon as something tries to access it. This will be output to the console (as below). @@ -146,6 +143,10 @@ It's suggested you add dependencies to these tasks to ensure your project is in reckonTagCreate.dependsOn check ``` +### Examples + +See [How Reckon Works](docs/index.md), which includes examples of how reckon will behave in various scenarios. + ## Contributing Contributions are very welcome and are accepted through pull requests. diff --git a/docs/index.md b/docs/index.md index b7d3c768..b7f7344b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,8 +1,6 @@ # How Reckon Works -## Inference Algorithm - -### Axioms +## Axioms These are the rules that reckon presumes are true, both informing how it reads a repo's history and how it calculates the next version: @@ -16,7 +14,7 @@ These are the rules that reckon presumes are true, both informing how it reads a 1. **Final versions MUST NOT be re-released as a pre-release.** Once you release a final version (e.g. 1.2.3), that same commit cannot be re-released as a pre-release (e.g. 1.3.0-beta.1). However, the commit can be re-released as a final (e.g. 1.3.0). 1. **Final and significant versions MUST be released from a clean repo.** If there are any uncommitted changes, the version will not be treated as a final or significant. -### Inputs +## Inputs In order to infer the next version, reckon needs two pieces of input: @@ -37,13 +35,13 @@ plugins { // ... reckon { - normal = scopeFromProp() - preRelease = stageFromProp('beta', 'rc', 'final') + scopeFromProp() + stageFromProp('beta', 'rc', 'final') } // ... ``` -**You have some changes in the repository, but no commits yet.** +### You have some changes in the repository, but no commits yet ``` $ ./gradlew build @@ -52,7 +50,7 @@ Reckoned version: 0.1.0-beta.0.0+uncommitted This used the default of `minor` scope and `beta` stage (`beta` is the first stage alphabetically). Since you have some changes in your repo that aren't committed, indicate that in the build -**Now make a commit, but run the same Gradle command.** +### Now make a commit, but run the same Gradle command ``` $ ./gradlew build @@ -61,7 +59,7 @@ Reckoned version: 0.1.0-beta.0.1+e06c68a863eb6ceaf889ee5802f478c10c1464bb The version now shows 1 commit since a normal has been released, and the full commit hash in the build metadata. -**Now make some more changes, but don't commit them** +### Now make some more changes, but don't commit them ``` $ ./gradlew build @@ -70,7 +68,7 @@ Reckoned version: 0.1.0-beta.0.1+e06c68a863eb6ceaf889ee5802f478c10c1464bb.uncomm The version hasn't changed except to indicate that you have uncommitted changes. -**Now commit this and let's release a minor version beta** +### Now commit this and let's release a minor version beta You can specify the scope or leave it off, since `minor` is the default. @@ -81,7 +79,7 @@ Reckoned version: 0.1.0-beta.1 ``` Note that you no longer have a count of commits or a commit hash, since this is a significant version that will result in a tag. -**Now just run the build again** +### Now just run the build again ``` $ ./gradlew build @@ -90,7 +88,7 @@ Reckoned version: 0.1.0-beta.1 The current `HEAD` is tagged and you haven't changed anything, or indicated you wanted a different version by providing scope or stage. Reckon assumes you just want to rebuild the existing version. -**Make a bunch more commits and build again** +### Make a bunch more commits and build again ``` $ ./gradlew build @@ -99,7 +97,7 @@ Reckoned version: 0.1.0-beta.1.8+e06c68a863eb6ceaf889ee5802f478c10c1464bb We're back to an insignificant version, since you didn't indicate a stage. Again we get the commit count and hash. -**Release another beta** +### Release another beta ``` $ ./gradlew build reckonTagPush -Preckon.stage=beta @@ -108,7 +106,7 @@ Reckoned version: 0.1.0-beta.2 While you already could have left the scope of with the default of `minor`, you can also leave it off because you just want to continue development towards the _target_ normal version you've been working on. -**Release this commit as an rc** +### Release this commit as an rc You've decided there's enough features in this release, and you're ready to treat it as a release-candidate. @@ -119,7 +117,7 @@ Reckoned version: 0.1.0-rc.1 Note that the count after the stage resets to 1. -**Make a bug fix but don't commit it yet** +### Make a bug fix but don't commit it yet ``` $ ./gradlew build @@ -128,14 +126,14 @@ Reckoned version: 0.1.0-rc.1.8+e06c68a863eb6ceaf889ee5802f478c10c1464bb.uncommit Note that the commit count does not reset (since it's based on commits since the last normal). -**Commit the change and release another rc** +### Commit the change and release another rc ``` $ ./gradlew build reckonTagPush -Preckon.stage=rc Reckoned version: 0.1.0-rc.2 ``` -**Release this as a final** +### Release this as a final You've decided there aren't any bugs in this release and you're ready to make it official. @@ -144,7 +142,7 @@ $ ./gradlew build reckonTagPush -Preckon.stage=final Reckoned version: 0.1.0 ``` -**Make this the 1.0.0** +### Make this the 1.0.0 You've decided this is feature complete and you're ready to make your 1.0.0 release. @@ -153,7 +151,7 @@ $ ./gradlew build reckonTagPush -Preckon.scope=major -Preckon.stage=final Reckoned version: 1.0.0 ``` -**Make some commits and build** +### Make some commits and build ``` $ ./gradlew build @@ -162,14 +160,14 @@ Reckoned version: 1.1.0-beta.0.4+7836cf7469dd00fe1035ea14ef1faaa7452cc5e0 Note that `minor` was again used as a default, same with `beta`, and that your commit count reset since a normal was released. -**Release this as a patch rc** +### Release this as a patch rc ``` $ ./gradlew build reckonTagPush -Preckon.scope=patch -Preckon.stage=rc Reckoned version: 1.0.1-rc.1 ``` -**Release as a final patch** +### Release as a final patch ``` $ ./gradlew build reckonTagPush -Preckon.stage=final @@ -178,7 +176,7 @@ Reckoned version: 1.0.1 While the default is usually `minor`, if you're already developing towards a `patch` or `major` those will be used as defaults instead. -**Make some changes but don't commit them and run again** +### Make some changes but don't commit them and run again ``` $ ./gradlew build reckonTagPush -Preckon.stage=final diff --git a/global.lock b/global.lock index 4c642217..a9765263 100644 --- a/global.lock +++ b/global.lock @@ -24,7 +24,7 @@ "org.ajoberstar.reckon:reckon-gradle", "org.ajoberstar.reckon:reckon-gradle" ], - "locked": "24.1-jre" + "locked": "25.1-jre" }, "junit:junit": { "firstLevelTransitive": [ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c44b679a..f6b961fd 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 32a96c59..16d28051 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-bin.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/reckon-core/src/main/java/org/ajoberstar/reckon/core/git/GitInventorySupplier.java b/reckon-core/src/main/java/org/ajoberstar/reckon/core/GitInventorySupplier.java similarity index 88% rename from reckon-core/src/main/java/org/ajoberstar/reckon/core/git/GitInventorySupplier.java rename to reckon-core/src/main/java/org/ajoberstar/reckon/core/GitInventorySupplier.java index b7838d72..7956e355 100644 --- a/reckon-core/src/main/java/org/ajoberstar/reckon/core/git/GitInventorySupplier.java +++ b/reckon-core/src/main/java/org/ajoberstar/reckon/core/GitInventorySupplier.java @@ -1,4 +1,4 @@ -package org.ajoberstar.reckon.core.git; +package org.ajoberstar.reckon.core; import java.io.IOException; import java.io.UncheckedIOException; @@ -12,10 +12,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import com.github.zafarkhaja.semver.Version; -import org.ajoberstar.reckon.core.VcsInventory; -import org.ajoberstar.reckon.core.VcsInventorySupplier; -import org.ajoberstar.reckon.core.Versions; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -33,7 +29,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public final class GitInventorySupplier implements VcsInventorySupplier { +/** + * Supplies an inventory of a Git repository. + * + * This is intentionally package private. + */ +final class GitInventorySupplier implements VcsInventorySupplier { private static final Logger logger = LoggerFactory.getLogger(GitInventorySupplier.class); private final Repository repo; @@ -47,13 +48,15 @@ public GitInventorySupplier(Repository repo, Function> this.repo = repo; this.tagParser = ref -> { String tagName = Repository.shortenRefName(ref.getName()); - return tagSelector.apply(tagName).flatMap(Versions::valueOf); + return tagSelector.apply(tagName).flatMap(Version::parse); }; } @Override public VcsInventory getInventory() { + // share this walk throughout to benefit from its caching try (RevWalk walk = new RevWalk(repo)) { + // saves on some performance as we don't really need the commit bodys walk.setRetainBody(false); ObjectId headObjectId = repo.getRefDatabase().getRef("HEAD").getObjectId(); @@ -108,7 +111,7 @@ private boolean isClean() { try { return new Git(repo).status().call().isClean(); } catch (GitAPIException e) { - logger.error("Failed to determine status of repository.", e); + logger.error("Failed to determine status of repository. Assuming not clean.", e); // TODO should this throw up? return false; } @@ -132,6 +135,7 @@ private Set getTaggedVersions(RevWalk walk) throws IOException { private Optional findCurrent(RevCommit head, Stream versions) { return versions .filter(version -> version.getCommit().equals(head)) + // if multiple tags on the head commit, we want the highest precedence one .max(Comparator.comparing(TaggedVersion::getVersion)); } @@ -157,8 +161,11 @@ private TaggedVersion findBase(RevWalk walk, RevCommit head, Stream findParallelCandidates(RevWalk walk, RevCommit head, Set candidates) { @@ -198,7 +205,7 @@ private Optional findParallel(RevWalk walk, RevCommit head, TaggedVersi && !taggedSinceMergeBase && !mergeBase.equals(head) && !mergeBase.equals(candidate.getCommit())) { - return Optional.of(Versions.getNormal(candidate.getVersion())); + return Optional.of(candidate.getVersion().getNormal()); } else { return Optional.empty(); } @@ -225,7 +232,7 @@ public RevCommit getCommit() { } public boolean isNormal() { - return Versions.isNormal(version); + return version.isFinal(); } @Override diff --git a/reckon-core/src/main/java/org/ajoberstar/reckon/core/NormalStrategy.java b/reckon-core/src/main/java/org/ajoberstar/reckon/core/NormalStrategy.java deleted file mode 100644 index 4f536e71..00000000 --- a/reckon-core/src/main/java/org/ajoberstar/reckon/core/NormalStrategy.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.ajoberstar.reckon.core; - -import com.github.zafarkhaja.semver.Version; - -@FunctionalInterface -public interface NormalStrategy { - Version reckonNormal(VcsInventory inventory); -} diff --git a/reckon-core/src/main/java/org/ajoberstar/reckon/core/PreReleaseStrategy.java b/reckon-core/src/main/java/org/ajoberstar/reckon/core/PreReleaseStrategy.java deleted file mode 100644 index dd053ee7..00000000 --- a/reckon-core/src/main/java/org/ajoberstar/reckon/core/PreReleaseStrategy.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.ajoberstar.reckon.core; - -import com.github.zafarkhaja.semver.Version; - -@FunctionalInterface -public interface PreReleaseStrategy { - Version reckonTargetVersion(VcsInventory inventory, Version targetNormal); -} diff --git a/reckon-core/src/main/java/org/ajoberstar/reckon/core/Reckoner.java b/reckon-core/src/main/java/org/ajoberstar/reckon/core/Reckoner.java index f66d6585..ec4abe7c 100644 --- a/reckon-core/src/main/java/org/ajoberstar/reckon/core/Reckoner.java +++ b/reckon-core/src/main/java/org/ajoberstar/reckon/core/Reckoner.java @@ -1,31 +1,226 @@ package org.ajoberstar.reckon.core; -import com.github.zafarkhaja.semver.Version; +import java.util.Arrays; +import java.util.Collections; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.eclipse.jgit.lib.Repository; + +/** + * Primary interface to Reckon. Use {@code builder} to get to an instance of {@code Reckoner}. + */ public final class Reckoner { - private Reckoner() { - // do not instantiate + public static final String FINAL_STAGE = "final"; + public static final String SNAPSHOT_STAGE = "snapshot"; + + private final VcsInventorySupplier inventorySupplier; + private final Function> scopeCalc; + private final BiFunction> stageCalc; + private final Set stages; + private final String defaultStage; + + private Reckoner(VcsInventorySupplier inventorySupplier, Function> scopeCalc, BiFunction> stageCalc, Set stages, String defaultStage) { + this.inventorySupplier = inventorySupplier; + this.scopeCalc = scopeCalc; + this.stageCalc = stageCalc; + this.stages = stages; + this.defaultStage = defaultStage; } - public static String reckon(VcsInventorySupplier inventorySupplier, NormalStrategy normalStrategy, PreReleaseStrategy preReleaseStrategy) { + /** + * Infers the version that reflects the current state of your repository. + * + * @return the reckoned version + */ + public Version reckon() { VcsInventory inventory = inventorySupplier.getInventory(); - Version targetNormal = normalStrategy.reckonNormal(inventory); - Version reckoned = preReleaseStrategy.reckonTargetVersion(inventory, targetNormal); + Version targetNormal = reckonNormal(inventory); + Version reckoned = reckonTargetVersion(inventory, targetNormal); if (inventory.getClaimedVersions().contains(reckoned) && !inventory.getCurrentVersion().map(reckoned::equals).orElse(false)) { throw new IllegalStateException("Reckoned version " + reckoned + " has already been released."); } + if (inventory.getClaimedVersions().contains(targetNormal) && !inventory.getCurrentVersion().filter(targetNormal::equals).isPresent() && reckoned.isSignificant()) { + throw new IllegalStateException("Reckoned target normal version " + targetNormal + " has already been released."); + } + if (inventory.getBaseVersion().compareTo(reckoned) > 0) { throw new IllegalStateException("Reckoned version " + reckoned + " is (and cannot be) less than base version " + inventory.getBaseVersion()); } inventory.getCurrentVersion().ifPresent(current -> { - if (inventory.isClean() && Versions.isNormal(current) && !Versions.isNormal(reckoned)) { + if (inventory.isClean() && current.isFinal() && !reckoned.isFinal()) { throw new IllegalStateException("Cannot re-release a final version " + current + " as a pre-release: " + reckoned); } }); - return reckoned.toString(); + return reckoned; + } + + private Version reckonNormal(VcsInventory inventory) { + Optional providedScope = scopeCalc.apply(inventory).filter(value -> !value.isEmpty()).map(Scope::from); + + Scope scope; + if (providedScope.isPresent()) { + scope = providedScope.get(); + } else { + Optional inferredScope = Scope.infer(inventory.getBaseNormal(), inventory.getBaseVersion()); + scope = inferredScope.orElse(Scope.MINOR); + } + + Version targetNormal = inventory.getBaseNormal().incrementNormal(scope); + + // if a version's already being developed on a parallel branch we'll skip it + if (inventory.getParallelNormals().contains(targetNormal)) { + targetNormal = targetNormal.incrementNormal(scope); + } + + return targetNormal; + } + + private Version reckonTargetVersion(VcsInventory inventory, Version targetNormal) { + String stage = stageCalc.apply(inventory, targetNormal) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .orElse(null); + + if (stage != null && !stages.contains(stage)) { + String message = String.format("Stage \"%s\" is not one of: %s", stage, stages); + throw new IllegalArgumentException(message); + } + + if (stage != null && !inventory.isClean()) { + throw new IllegalStateException("Cannot release a final or significant stage without a clean repo."); + } + + if (FINAL_STAGE.equals(stage)) { + return targetNormal; + } + + // rebuild behavior + if (inventory.isClean() && inventory.getCurrentVersion().isPresent() && stage == null) { + Version current = inventory.getCurrentVersion().get(); + return current; + } + + Version targetBase = targetNormal.equals(inventory.getBaseVersion().getNormal()) ? inventory.getBaseVersion() : targetNormal; + String baseStageName = targetBase.getStage().map(Version.Stage::getName).orElse(defaultStage); + int baseStageNum = targetBase.getStage().map(Version.Stage::getNum).orElse(0); + + if (SNAPSHOT_STAGE.equals(defaultStage)) { + return Version.valueOf(String.format("%s-%s", targetBase.getNormal(), "SNAPSHOT")); + } else if (stage == null) { + String buildMetadata = inventory.getCommitId() + .map(sha -> inventory.isClean() ? sha : sha + ".uncommitted") + .orElse("uncommitted"); + + return Version.valueOf(String.format("%s-%s.%d.%d+%s", targetBase.getNormal(), baseStageName, baseStageNum, inventory.getCommitsSinceBase(), buildMetadata)); + } else if (stage.equals(baseStageName)) { + return Version.valueOf(String.format("%s-%s.%d", targetBase.getNormal(), baseStageName, baseStageNum + 1)); + } else { + return Version.valueOf(String.format("%s-%s.%d", targetBase.getNormal(), stage, 1)); + } + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private VcsInventorySupplier inventorySupplier; + private Function> scopeCalc; + private BiFunction> stageCalc; + private Set stages; + private String defaultStage; + + Builder vcs(VcsInventorySupplier inventorySupplier) { + this.inventorySupplier = inventorySupplier; + return this; + } + + /** + * Use the given JGit repository to infer the state of Git. + * + * @param repo repository that the version should be inferred from + * @return this builder + */ + public Builder git(Repository repo) { + if (repo == null) { + this.inventorySupplier = () -> new VcsInventory(null, false, null, null, null, 0, Collections.emptySet(), Collections.emptySet()); + } else { + this.inventorySupplier = new GitInventorySupplier(repo); + } + return this; + } + + /** + * Use the given function to determine what scope should be used when inferring the version. + * + * @param scopeCalc the function that provides the scope + * @return this builder + */ + public Builder scopeCalc(Function> scopeCalc) { + this.scopeCalc = scopeCalc; + return this; + } + + /** + * Use the given stages as valid options during inference. + * + * @param stages the valid stages + * @return this builder + */ + public Builder stages(String... stages) { + this.stages = Arrays.stream(stages).collect(Collectors.toSet()); + this.defaultStage = this.stages.stream() + .filter(name -> !FINAL_STAGE.equals(name)) + .sorted() + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No non-final stages provided.")); + return this; + } + + /** + * Use only the {@code snapshot} and {@code final} stages. Alternative to calling {@code stages()}. + * + * @return this builder + */ + public Builder snapshots() { + this.stages = Stream.of(Reckoner.FINAL_STAGE, Reckoner.SNAPSHOT_STAGE).collect(Collectors.toSet()); + this.defaultStage = Reckoner.SNAPSHOT_STAGE; + return this; + } + + /** + * Use the given function to determine what staged should be used when inferring the version. This + * must return a version contained in {@code stages()}. + * + * @param stageCalc the function that provides the stage + * @return this builder + */ + public Builder stageCalc(BiFunction> stageCalc) { + this.stageCalc = stageCalc; + return this; + } + + /** + * Builds the reckoner. + * + * @return the reckoner + */ + public Reckoner build() { + Objects.requireNonNull(inventorySupplier, "Must provide a vcs."); + Objects.requireNonNull(scopeCalc, "Must provide a scope supplier."); + Objects.requireNonNull(stages, "Must provide set of stages."); + Objects.requireNonNull(stageCalc, "Must provide a stage supplier."); + return new Reckoner(inventorySupplier, scopeCalc, stageCalc, stages, defaultStage); + } } } diff --git a/reckon-core/src/main/java/org/ajoberstar/reckon/core/Scope.java b/reckon-core/src/main/java/org/ajoberstar/reckon/core/Scope.java index 6a744da1..c08bb408 100644 --- a/reckon-core/src/main/java/org/ajoberstar/reckon/core/Scope.java +++ b/reckon-core/src/main/java/org/ajoberstar/reckon/core/Scope.java @@ -1,11 +1,25 @@ package org.ajoberstar.reckon.core; import java.util.Arrays; +import java.util.Optional; import java.util.stream.Collectors; +/** + * Scope is an indication of how large the change between SemVer normal versions is. + */ public enum Scope { MAJOR, MINOR, PATCH; + /** + * Parses a String version of a Scope. This is an alternative to {@code valueOf} which only supports + * literal matches. This method supports mixed case String representations, like Major or minor, + * instead of just PATCH. Additionally, it provides a better error message when an invalid scope is + * provided. + * + * @param value the string to parse as a scope + * @return the matching scope + * @throws IllegalArgumentException if no match was found + */ public static Scope from(String value) { try { return Scope.valueOf(value.toUpperCase()); @@ -18,4 +32,30 @@ public static Scope from(String value) { throw new IllegalArgumentException(message, e); } } + + /** + * Infers the scope between two versions. It looks left-to-right through the components of the + * normal versions looking for the first difference of 1 that it finds. Anything else is considered + * an invalid difference. For example, 1.0.0 to 2.0.0 is MAJOR, 1.0.0 to 1.1.0 is MINOR, 1.0.0 to + * 1.0.1 is PATCH, 1.0.0 to 3.0.0 is invalid. + * + * @param before the earlier version to compare + * @param after the later version to compare + * @return the scope of the change between the two versions, or empty if they are the same or have + * an invalid increment + */ + public static Optional infer(Version before, Version after) { + int major = after.getVersion().getMajorVersion() - before.getVersion().getMajorVersion(); + int minor = after.getVersion().getMinorVersion() - before.getVersion().getMinorVersion(); + int patch = after.getVersion().getPatchVersion() - before.getVersion().getPatchVersion(); + if (major == 1 && after.getVersion().getMinorVersion() == 0 && after.getVersion().getPatchVersion() == 0) { + return Optional.of(Scope.MAJOR); + } else if (major == 0 && minor == 1 && after.getVersion().getPatchVersion() == 0) { + return Optional.of(Scope.MINOR); + } else if (major == 0 && minor == 0 && patch == 1) { + return Optional.of(Scope.PATCH); + } else { + return Optional.empty(); + } + } } diff --git a/reckon-core/src/main/java/org/ajoberstar/reckon/core/VcsInventory.java b/reckon-core/src/main/java/org/ajoberstar/reckon/core/VcsInventory.java index e0cb2514..3ea55dbb 100644 --- a/reckon-core/src/main/java/org/ajoberstar/reckon/core/VcsInventory.java +++ b/reckon-core/src/main/java/org/ajoberstar/reckon/core/VcsInventory.java @@ -4,12 +4,14 @@ import java.util.Optional; import java.util.Set; -import com.github.zafarkhaja.semver.Version; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; +/** + * An inventory of the state of your VCS and the versions tagged within it. + */ public final class VcsInventory { private final String commitId; private final boolean clean; @@ -20,7 +22,10 @@ public final class VcsInventory { private final Set parallelNormals; private final Set claimedVersions; - public VcsInventory( + /** + * This is intentionally package private. + */ + VcsInventory( String commitId, boolean clean, Version currentVersion, @@ -36,8 +41,8 @@ public VcsInventory( this.commitId = commitId; this.clean = clean; this.currentVersion = currentVersion; - this.baseVersion = Optional.ofNullable(baseVersion).orElse(Versions.VERSION_0); - this.baseNormal = Optional.ofNullable(baseNormal).orElse(Versions.VERSION_0); + this.baseVersion = Optional.ofNullable(baseVersion).orElse(Version.IDENTITY); + this.baseNormal = Optional.ofNullable(baseNormal).orElse(Version.IDENTITY); this.commitsSinceBase = commitsSinceBase; this.parallelNormals = Optional.ofNullable(parallelNormals) .map(Collections::unmodifiableSet) @@ -47,34 +52,59 @@ public VcsInventory( .orElse(Collections.emptySet()); } + /** + * The ID of the current commit, if any, in the active branch of the repository. + */ public Optional getCommitId() { return Optional.ofNullable(commitId); } + /** + * Whether the repository has any uncommitted changes. + */ public boolean isClean() { return clean; } + /** + * Gets the version tagged on the current commit of the repository, if any. + */ public Optional getCurrentVersion() { return Optional.ofNullable(currentVersion); } + /** + * Number of commits between the current commmit and the base normal version tag. + */ public int getCommitsSinceBase() { return commitsSinceBase; } + /** + * The most recent (based on ancestry, not time) tagged version from the current commit. May be a + * pre-release version, but could be the same as baseNormal. + */ public Version getBaseVersion() { return baseVersion; } + /** + * The most recent (based on ancestry, not time) tagged final version from the current commit. + */ public Version getBaseNormal() { return baseNormal; } + /** + * Any normal versions under development in other branches. + */ public Set getParallelNormals() { return parallelNormals; } + /** + * Any versions that have already been released or otherwise claimed. + */ public Set getClaimedVersions() { return claimedVersions; } diff --git a/reckon-core/src/main/java/org/ajoberstar/reckon/core/VcsInventorySupplier.java b/reckon-core/src/main/java/org/ajoberstar/reckon/core/VcsInventorySupplier.java index 56f0e69d..25fd775d 100644 --- a/reckon-core/src/main/java/org/ajoberstar/reckon/core/VcsInventorySupplier.java +++ b/reckon-core/src/main/java/org/ajoberstar/reckon/core/VcsInventorySupplier.java @@ -1,6 +1,9 @@ package org.ajoberstar.reckon.core; +/** + * This is intentionally package private. + */ @FunctionalInterface -public interface VcsInventorySupplier { +interface VcsInventorySupplier { VcsInventory getInventory(); } diff --git a/reckon-core/src/main/java/org/ajoberstar/reckon/core/Version.java b/reckon-core/src/main/java/org/ajoberstar/reckon/core/Version.java new file mode 100644 index 00000000..4f5c5ddd --- /dev/null +++ b/reckon-core/src/main/java/org/ajoberstar/reckon/core/Version.java @@ -0,0 +1,180 @@ +package org.ajoberstar.reckon.core; + +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.github.zafarkhaja.semver.ParseException; + +/** + * A SemVer-compliant version. + */ +public final class Version implements Comparable { + /** + * A base version for use as a default in cases where you don't have an existing version. + */ + public static final Version IDENTITY = new Version(com.github.zafarkhaja.semver.Version.forIntegers(0, 0, 0)); + + private final com.github.zafarkhaja.semver.Version version; + private final Version normal; + private final Stage stage; + + private Version(com.github.zafarkhaja.semver.Version version) { + this.version = version; + // need this if logic to avoid stack overflow + if (version.getPreReleaseVersion().isEmpty()) { + this.normal = this; + } else { + this.normal = new Version(com.github.zafarkhaja.semver.Version.forIntegers(version.getMajorVersion(), version.getMinorVersion(), version.getPatchVersion())); + } + this.stage = Stage.valueOf(version); + } + + /** + * This is intentionally package private. + * + * @return the internal JSemver version + */ + com.github.zafarkhaja.semver.Version getVersion() { + return version; + } + + /** + * @return the normal component of the version. + */ + public Version getNormal() { + return normal; + } + + /** + * @return the stage of this version, if any. + */ + public Optional getStage() { + return Optional.ofNullable(stage); + } + + /** + * @return {@code true} if this is a final version (i.e. doesn't have pre-release information), + * {@code false} otherwise + */ + public boolean isFinal() { + return version.getPreReleaseVersion().isEmpty(); + } + + /** + * @return {@code true} if the version is final or any other significant stage, {@code false} if + * insignficant or snapshot + */ + public boolean isSignificant() { + return isFinal() || getStage() + .filter(stage -> !"SNAPSHOT".equals(stage.getName())) + .filter(stage -> version.getBuildMetadata().isEmpty()) + .isPresent(); + } + + /** + * Increments this version using the given scope to get a new normal version. + * + * @param scope the scope to increment the version by + * @return incremented version, with only the normal component + */ + public Version incrementNormal(Scope scope) { + switch (scope) { + case MAJOR: + return new Version(version.incrementMajorVersion()); + case MINOR: + return new Version(version.incrementMinorVersion()); + case PATCH: + return new Version(version.incrementPatchVersion()); + default: + throw new AssertionError("Invalid scope: " + scope); + } + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } else if (obj instanceof Version) { + return Objects.equals(this.version, ((Version) obj).version); + } else { + return false; + } + } + + @Override + public int hashCode() { + return version.hashCode(); + } + + @Override + public int compareTo(Version that) { + return this.version.compareTo(that.version); + } + + @Override + public String toString() { + return version.toString(); + } + + /** + * Stage of development. + */ + public static final class Stage { + private static final Pattern STAGE_REGEX = Pattern.compile("^(?\\w+)(?:\\.(?\\d+))?"); + + private final String name; + private final int num; + + private Stage(String name, int num) { + this.name = name; + this.num = num; + } + + public String getName() { + return name; + } + + public int getNum() { + return num; + } + + private static Stage valueOf(com.github.zafarkhaja.semver.Version version) { + Matcher matcher = STAGE_REGEX.matcher(version.getPreReleaseVersion()); + if (matcher.find()) { + String name = matcher.group("name"); + int num = Optional.ofNullable(matcher.group("num")).map(Integer::parseInt).orElse(0); + return new Stage(name, num); + } else { + return null; + } + } + } + + /** + * Gets the version represented by the given string. If the version is not SemVer compliant, an + * exception will be thrown. Use {@code parse} if you don't trust that your input is valid. + * + * @param versionString version to parse + * @return the version + */ + public static Version valueOf(String versionString) { + return new Version(com.github.zafarkhaja.semver.Version.valueOf(versionString)); + } + + /** + * Gets the version represented by the given string, if it's SemVer compliant. If not, an empty + * Optional will be returned. + * + * @param versionString version to parse + * @return the version or an empty optional, if the string wasn't SemVer compliant + */ + public static Optional parse(String versionString) { + try { + return Optional.of(new Version(com.github.zafarkhaja.semver.Version.valueOf(versionString))); + } catch (IllegalArgumentException | ParseException e) { + return Optional.empty(); + } + } +} diff --git a/reckon-core/src/main/java/org/ajoberstar/reckon/core/Versions.java b/reckon-core/src/main/java/org/ajoberstar/reckon/core/Versions.java deleted file mode 100644 index 29630eac..00000000 --- a/reckon-core/src/main/java/org/ajoberstar/reckon/core/Versions.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.ajoberstar.reckon.core; - -import java.util.Optional; - -import com.github.zafarkhaja.semver.ParseException; -import com.github.zafarkhaja.semver.Version; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public final class Versions { - private static final Logger logger = LoggerFactory.getLogger(Versions.class); - - public static final Version VERSION_0 = Version.forIntegers(0, 0, 0); - - private Versions() { - // do not instantiate - } - - public static Optional valueOf(String version) { - try { - return Optional.of(Version.valueOf(version)); - } catch (IllegalArgumentException | ParseException e) { - logger.debug("Cannot parse {} as version.", version, e); - return Optional.empty(); - } - } - - public static boolean isNormal(Version version) { - return version.getPreReleaseVersion().isEmpty(); - } - - public static Version getNormal(Version version) { - return Version.forIntegers( - version.getMajorVersion(), version.getMinorVersion(), version.getPatchVersion()); - } - - public static Version incrementNormal(Version version, Scope scope) { - switch (scope) { - case MAJOR: - return version.incrementMajorVersion(); - case MINOR: - return version.incrementMinorVersion(); - case PATCH: - return version.incrementPatchVersion(); - default: - throw new AssertionError("Invalid scope: " + scope); - } - } - - public static Optional inferScope(Version before, Version after) { - int major = after.getMajorVersion() - before.getMajorVersion(); - int minor = after.getMinorVersion() - before.getMinorVersion(); - int patch = after.getPatchVersion() - before.getPatchVersion(); - if (major == 1 && after.getMinorVersion() == 0 && after.getPatchVersion() == 0) { - return Optional.of(Scope.MAJOR); - } else if (major == 0 && minor == 1 && after.getPatchVersion() == 0) { - return Optional.of(Scope.MINOR); - } else if (major == 0 && minor == 0 && patch == 1) { - return Optional.of(Scope.PATCH); - } else { - logger.debug("Invalid increment between the following versions. Cannot infer scope between: {} -> {}", before, after); - return Optional.empty(); - } - } -} diff --git a/reckon-core/src/main/java/org/ajoberstar/reckon/core/strategy/ScopeNormalStrategy.java b/reckon-core/src/main/java/org/ajoberstar/reckon/core/strategy/ScopeNormalStrategy.java deleted file mode 100644 index c6f7b577..00000000 --- a/reckon-core/src/main/java/org/ajoberstar/reckon/core/strategy/ScopeNormalStrategy.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.ajoberstar.reckon.core.strategy; - -import java.util.Optional; -import java.util.function.Function; - -import com.github.zafarkhaja.semver.Version; -import org.ajoberstar.reckon.core.NormalStrategy; -import org.ajoberstar.reckon.core.Scope; -import org.ajoberstar.reckon.core.VcsInventory; -import org.ajoberstar.reckon.core.Versions; - -public class ScopeNormalStrategy implements NormalStrategy { - private final Function> scopeCalc; - - public ScopeNormalStrategy(Function> scopeCalc) { - this.scopeCalc = scopeCalc; - } - - @Override - public Version reckonNormal(VcsInventory inventory) { - Optional providedScope = scopeCalc.apply(inventory).filter(value -> !value.isEmpty()).map(Scope::from); - - Scope scope; - if (providedScope.isPresent()) { - scope = providedScope.get(); - } else { - Optional inferredScope = Versions.inferScope(inventory.getBaseNormal(), inventory.getBaseVersion()); - scope = inferredScope.orElse(Scope.MINOR); - } - - Version targetNormal = Versions.incrementNormal(inventory.getBaseNormal(), scope); - - // if a version's already being developed on a parallel branch we'll skip it - if (inventory.getParallelNormals().contains(targetNormal)) { - targetNormal = Versions.incrementNormal(targetNormal, scope); - } - - if (inventory.getClaimedVersions().contains(targetNormal)) { - throw new IllegalStateException("Reckoned target normal version " + targetNormal + " has already been released."); - } - - return targetNormal; - } -} diff --git a/reckon-core/src/main/java/org/ajoberstar/reckon/core/strategy/SnapshotPreReleaseStrategy.java b/reckon-core/src/main/java/org/ajoberstar/reckon/core/strategy/SnapshotPreReleaseStrategy.java deleted file mode 100644 index 3fa1a2d2..00000000 --- a/reckon-core/src/main/java/org/ajoberstar/reckon/core/strategy/SnapshotPreReleaseStrategy.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.ajoberstar.reckon.core.strategy; - -import java.util.Optional; -import java.util.function.BiFunction; - -import com.github.zafarkhaja.semver.Version; -import org.ajoberstar.reckon.core.PreReleaseStrategy; -import org.ajoberstar.reckon.core.VcsInventory; - -public final class SnapshotPreReleaseStrategy implements PreReleaseStrategy { - public static final String FINAL_STAGE = "final"; - public static final String SNAPSHOT_STAGE = "snapshot"; - - private final BiFunction> stageCalc; - - public SnapshotPreReleaseStrategy(BiFunction> stageCalc) { - this.stageCalc = stageCalc; - } - - @Override - public Version reckonTargetVersion(VcsInventory inventory, Version targetNormal) { - Optional maybeStage = stageCalc.apply(inventory, targetNormal); - - if (inventory.isClean() && inventory.getCurrentVersion().isPresent() && !maybeStage.isPresent()) { - // rebuild - return inventory.getCurrentVersion().get(); - } else { - String stage = maybeStage.orElse(SNAPSHOT_STAGE); - if (stage.equals(SNAPSHOT_STAGE)) { - return targetNormal.setPreReleaseVersion("SNAPSHOT"); - } else if (!stage.equals(FINAL_STAGE)) { - throw new IllegalArgumentException(String.format("Stage \"%s\" is not one of: [snapshot, final]", stage)); - } else if (!inventory.isClean()) { - throw new IllegalStateException("Cannot release a final version without a clean repo."); - } else { - return targetNormal; - } - } - } -} diff --git a/reckon-core/src/main/java/org/ajoberstar/reckon/core/strategy/StagePreReleaseStrategy.java b/reckon-core/src/main/java/org/ajoberstar/reckon/core/strategy/StagePreReleaseStrategy.java deleted file mode 100644 index ec63c097..00000000 --- a/reckon-core/src/main/java/org/ajoberstar/reckon/core/strategy/StagePreReleaseStrategy.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.ajoberstar.reckon.core.strategy; - -import java.util.Optional; -import java.util.Set; -import java.util.function.BiFunction; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import com.github.zafarkhaja.semver.Version; -import org.ajoberstar.reckon.core.PreReleaseStrategy; -import org.ajoberstar.reckon.core.VcsInventory; -import org.ajoberstar.reckon.core.Versions; - -public final class StagePreReleaseStrategy implements PreReleaseStrategy { - public static final String FINAL_STAGE = "final"; - private static final Pattern STAGE_REGEX = Pattern.compile("^(?\\w+)(?:\\.(?\\d+))?"); - - private final Set stages; - private final String defaultStage; - private final BiFunction> stageCalc; - - public StagePreReleaseStrategy(Set stages, BiFunction> stageCalc) { - this.stages = stages; - this.defaultStage = stages.stream() - .filter(name -> !FINAL_STAGE.equals(name)) - .sorted() - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("No non-final stages provided.")); - this.stageCalc = stageCalc; - } - - @Override - public Version reckonTargetVersion(VcsInventory inventory, Version targetNormal) { - String stage = stageCalc.apply(inventory, targetNormal) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .orElse(null); - - if (stage != null && !stages.contains(stage)) { - String message = String.format("Stage \"%s\" is not one of: %s", stage, stages); - throw new IllegalArgumentException(message); - } - - if (stage != null && !inventory.isClean()) { - throw new IllegalStateException("Cannot release a final or significant stage without a clean repo."); - } - - if (FINAL_STAGE.equals(stage)) { - return targetNormal; - } - - // rebuild behavior - if (inventory.isClean() && inventory.getCurrentVersion().isPresent() && stage == null) { - Version current = inventory.getCurrentVersion().get(); - return current; - } - - Version targetBase = targetNormal.equals(Versions.getNormal(inventory.getBaseVersion())) ? inventory.getBaseVersion() : targetNormal; - - String baseStageName; - int baseStageNum; - Matcher matcher = STAGE_REGEX.matcher(targetBase.getPreReleaseVersion()); - if (matcher.find()) { - baseStageName = matcher.group("name"); - baseStageNum = Optional.ofNullable(matcher.group("num")).map(Integer::parseInt).orElse(0); - } else { - baseStageName = defaultStage; - baseStageNum = 0; - } - - if (stage == null) { - String buildMetadata = inventory.getCommitId() - .map(sha -> inventory.isClean() ? sha : sha + ".uncommitted") - .orElse("uncommitted"); - - return targetBase - .setPreReleaseVersion(baseStageName + "." + baseStageNum + "." + inventory.getCommitsSinceBase()) - .setBuildMetadata(buildMetadata); - } else if (stage.equals(baseStageName)) { - int num = baseStageNum > 0 ? baseStageNum + 1 : 1; - return targetBase.setPreReleaseVersion(stage + "." + num); - } else { - return targetBase.setPreReleaseVersion(stage + ".1"); - } - } -} diff --git a/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/git/GitInventorySupplierTest.groovy b/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/GitInventorySupplierTest.groovy similarity index 80% rename from reckon-core/src/test/groovy/org/ajoberstar/reckon/core/git/GitInventorySupplierTest.groovy rename to reckon-core/src/test/groovy/org/ajoberstar/reckon/core/GitInventorySupplierTest.groovy index e5f0a657..6a6a26b0 100644 --- a/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/git/GitInventorySupplierTest.groovy +++ b/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/GitInventorySupplierTest.groovy @@ -1,25 +1,8 @@ -/* - * Copyright 2015-2017 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.ajoberstar.reckon.core.git +package org.ajoberstar.reckon.core import java.nio.file.Files import java.security.SecureRandom import org.ajoberstar.grgit.Grgit -import org.ajoberstar.reckon.core.VcsInventory -import org.ajoberstar.reckon.core.Versions import spock.lang.Shared import spock.lang.Specification @@ -43,28 +26,28 @@ class GitInventorySupplierTest extends Specification { given: checkout('head-single-tag') expect: - supplier.getInventory().currentVersion == Versions.valueOf('0.1.0-milestone.1') + supplier.getInventory().currentVersion == Version.parse('0.1.0-milestone.1') } def 'if multiple tagged version on HEAD, the max is current version'() { given: checkout('head-multi-tag') expect: - supplier.getInventory().currentVersion == Versions.valueOf('0.1.0') + supplier.getInventory().currentVersion == Version.parse('0.1.0') } def 'if no tagged finals in HEAD\'s history, base normal is 0.0.0'() { given: checkout('final-unreachable') expect: - supplier.getInventory().baseNormal == Versions.valueOf('0.0.0').get() + supplier.getInventory().baseNormal == Version.parse('0.0.0').get() } def 'if tagged finals in HEAD\'s history, base normal is max of finals which have no other final between them and HEAD'() { given: checkout('final-reachable') expect: - supplier.getInventory().baseNormal == Versions.valueOf('1.0.0').get() + supplier.getInventory().baseNormal == Version.parse('1.0.0').get() } def 'if tagged finals on head, base normal and version are same as current version'() { @@ -81,14 +64,14 @@ class GitInventorySupplierTest extends Specification { given: checkout('version-unreachable') expect: - supplier.getInventory().baseVersion == Versions.valueOf('0.0.0').get() + supplier.getInventory().baseVersion == Version.parse('0.0.0').get() } def 'if tagged versions in HEAD\'s history, base version is max of versions which have no other version between them and HEAD'() { given: checkout('version-reachable') expect: - supplier.getInventory().baseVersion == Versions.valueOf('0.3.0-milestone.1').get() + supplier.getInventory().baseVersion == Version.parse('0.3.0-milestone.1').get() } def 'if tagged versions on head, base version is same as current version'() { @@ -139,19 +122,19 @@ class GitInventorySupplierTest extends Specification { given: checkout('parallel-untagged-since-merge') expect: - supplier.getInventory().parallelNormals == [Versions.valueOf('0.2.0').get()] as Set + supplier.getInventory().parallelNormals == [Version.parse('0.2.0').get()] as Set } def 'all tagged versions treated as claimed versions'() { expect: supplier.getInventory().claimedVersions == [ - Versions.valueOf('0.1.0-milestone.1').get(), - Versions.valueOf('0.1.0-rc.1').get(), - Versions.valueOf('0.1.0').get(), - Versions.valueOf('0.2.0-rc.1').get(), - Versions.valueOf('0.3.0-milestone.1').get(), - Versions.valueOf('0.3.0').get(), - Versions.valueOf('1.0.0').get() + Version.parse('0.1.0-milestone.1').get(), + Version.parse('0.1.0-rc.1').get(), + Version.parse('0.1.0').get(), + Version.parse('0.2.0-rc.1').get(), + Version.parse('0.3.0-milestone.1').get(), + Version.parse('0.3.0').get(), + Version.parse('1.0.0').get() ] as Set } diff --git a/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/ReckonerTest.groovy b/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/ReckonerTest.groovy index be10960e..27007479 100644 --- a/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/ReckonerTest.groovy +++ b/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/ReckonerTest.groovy @@ -1,25 +1,7 @@ -/* - * Copyright 2015-2017 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.ajoberstar.reckon.core -import com.github.zafarkhaja.semver.Version import spock.lang.Specification -import org.ajoberstar.reckon.core.strategy.ScopeNormalStrategy; -import org.ajoberstar.reckon.core.strategy.StagePreReleaseStrategy; -import org.ajoberstar.reckon.core.strategy.SnapshotPreReleaseStrategy; +import spock.lang.Unroll class ReckonerTest extends Specification { def 'if version is claimed, throw'() { @@ -34,47 +16,15 @@ class ReckonerTest extends Specification { [Version.valueOf('1.3.0')] as Set, [Version.valueOf('2.0.0-rc.1')] as Set ) - - VcsInventorySupplier inventorySupplier = Mock() - inventorySupplier.getInventory() >> inventory - NormalStrategy normal = Mock() - normal.reckonNormal(inventory) >> Version.valueOf('2.0.0') - PreReleaseStrategy preRelease = Mock() - preRelease.reckonTargetVersion(inventory, Version.valueOf('2.0.0')) >> Version.valueOf('2.0.0-rc.1') - when: - Reckoner.reckon(inventorySupplier, normal, preRelease) - then: - thrown(IllegalStateException) - } - - def 'if version is not greater than base, throw'() { - given: - VcsInventory inventory = new VcsInventory( - 'abcdef', - true, - null, - Version.valueOf('1.2.3-milestone.1'), - Version.valueOf('1.2.2'), - 1, - [Version.valueOf('1.3.0')] as Set, - [Version.valueOf('2.0.0-rc.1')] as Set - ) - - VcsInventorySupplier inventorySupplier = Mock() - inventorySupplier.getInventory() >> inventory - NormalStrategy normal = Mock() - normal.reckonNormal(inventory) >> Version.valueOf('1.0.0') - PreReleaseStrategy preRelease = Mock() - preRelease.reckonTargetVersion(inventory, Version.valueOf('1.0.0')) >> Version.valueOf('1.0.0-rc.1') when: - Reckoner.reckon(inventorySupplier, normal, preRelease) + reckonStage(inventory, 'major', 'rc') then: thrown(IllegalStateException) } def 'if target version is normal, current version is ignored'() { given: - VcsInventory inventory2 = new VcsInventory( + VcsInventory inventory = new VcsInventory( 'abcdef', true, Version.valueOf('1.2.3-milestone.1'), @@ -85,12 +35,12 @@ class ReckonerTest extends Specification { [Version.valueOf('1.2.2'), Version.valueOf('1.2.3-milestone.1')] as Set ) expect: - reckonStage(inventory2, 'major', 'final') == '2.0.0' + reckonStage(inventory, 'major', 'final') == '2.0.0' } def 'if current version is present and pre-release, repo is clean, and no input provided, this is a rebuild'() { given: - VcsInventory inventory2 = new VcsInventory( + VcsInventory inventory = new VcsInventory( 'abcdef', true, Version.valueOf('1.2.3-milestone.1'), @@ -101,12 +51,12 @@ class ReckonerTest extends Specification { [Version.valueOf('1.2.2'), Version.valueOf('1.2.3-milestone.1')] as Set ) expect: - reckonStage(inventory2, null, null) == '1.2.3-milestone.1' + reckonStage(inventory, null, null) == '1.2.3-milestone.1' } def 'if current version is present and normal, repo is clean, and no input provided, this is a rebuild'() { given: - VcsInventory inventory2 = new VcsInventory( + VcsInventory inventory = new VcsInventory( 'abcdef', true, Version.valueOf('1.2.3'), @@ -117,13 +67,13 @@ class ReckonerTest extends Specification { [Version.valueOf('1.2.2'), Version.valueOf('1.2.3-milestone.1')] as Set ) expect: - reckonStage(inventory2, null, null) == '1.2.3' - reckonSnapshot(inventory2, null, null) == '1.2.3' + reckonStage(inventory, null, null) == '1.2.3' + reckonSnapshot(inventory, null, null) == '1.2.3' } def 'if current version is present and pre-release, repo is dirty, and no input provided, this is not a rebuild'() { given: - VcsInventory inventory2 = new VcsInventory( + VcsInventory inventory = new VcsInventory( 'abcdef', false, Version.valueOf('1.2.3-milestone.1'), @@ -134,12 +84,12 @@ class ReckonerTest extends Specification { [Version.valueOf('1.2.2'), Version.valueOf('1.2.3-milestone.1')] as Set ) expect: - reckonStage(inventory2, null, null) == '1.2.3-milestone.1.1+abcdef.uncommitted' + reckonStage(inventory, null, null) == '1.2.3-milestone.1.1+abcdef.uncommitted' } def 'if current version is present and normal, repo is dirty, and no input provided, this is not a rebuild'() { given: - VcsInventory inventory2 = new VcsInventory( + VcsInventory inventory = new VcsInventory( 'abcdef', false, Version.valueOf('1.2.3'), @@ -150,13 +100,13 @@ class ReckonerTest extends Specification { [Version.valueOf('1.2.2'), Version.valueOf('1.2.3')] as Set ) expect: - reckonStage(inventory2, null, null) == '1.3.0-beta.0.1+abcdef.uncommitted' - reckonSnapshot(inventory2, null, null) == '1.3.0-SNAPSHOT' + reckonStage(inventory, null, null) == '1.3.0-beta.0.1+abcdef.uncommitted' + reckonSnapshot(inventory, null, null) == '1.3.0-SNAPSHOT' } def 'if current version is present and normal, repo is clean, allowed to release an incremented final'() { given: - VcsInventory inventory2 = new VcsInventory( + VcsInventory inventory = new VcsInventory( 'abcdef', true, Version.valueOf('1.2.3'), @@ -167,13 +117,13 @@ class ReckonerTest extends Specification { [Version.valueOf('1.2.2'), Version.valueOf('1.2.3')] as Set ) expect: - reckonStage(inventory2, 'minor', 'final') == '1.3.0' - reckonSnapshot(inventory2, 'major', 'final') == '2.0.0' + reckonStage(inventory, 'minor', 'final') == '1.3.0' + reckonSnapshot(inventory, 'major', 'final') == '2.0.0' } def 'if current version is present and normal, repo is clean, not allowed to release an incremented pre-release stage'() { given: - VcsInventory inventory2 = new VcsInventory( + VcsInventory inventory = new VcsInventory( 'abcdef', true, Version.valueOf('1.2.3'), @@ -184,14 +134,14 @@ class ReckonerTest extends Specification { [Version.valueOf('1.2.2'), Version.valueOf('1.2.3')] as Set ) when: - reckonStage(inventory2, null, 'rc') + reckonStage(inventory, null, 'rc') then: thrown(IllegalStateException) } def 'if current version is present and normal, repo is clean, not allowed to release an incremented snapshot'() { given: - VcsInventory inventory2 = new VcsInventory( + VcsInventory inventory = new VcsInventory( 'abcdef', true, Version.valueOf('1.2.3'), @@ -202,14 +152,14 @@ class ReckonerTest extends Specification { [Version.valueOf('1.2.2'), Version.valueOf('1.2.3')] as Set ) when: - reckonSnapshot(inventory2, null, 'snapshot') + reckonSnapshot(inventory, null, 'snapshot') then: thrown(IllegalStateException) } def 'if current version is present and pre-release, repo is clean, allowed to release a higher normal pre-release'() { given: - VcsInventory inventory2 = new VcsInventory( + VcsInventory inventory = new VcsInventory( 'abcdef', true, Version.valueOf('1.2.3-milestone.1'), @@ -220,18 +170,415 @@ class ReckonerTest extends Specification { [Version.valueOf('1.2.2'), Version.valueOf('1.2.3-milestone.1')] as Set ) expect: - reckonStage(inventory2, 'minor', 'rc') == '1.3.0-rc.1' + reckonStage(inventory, 'minor', 'rc') == '1.3.0-rc.1' + } + + def 'rebuilding claimed version succeeds, if repo is clean'() { + given: + def inventory = new VcsInventory( + 'abcdef', + true, + Version.valueOf('0.1.0'), + Version.valueOf('0.0.0'), + Version.valueOf('0.0.0'), + 1, + [] as Set, + [Version.valueOf('0.1.0'), Version.valueOf('0.1.1'), Version.valueOf('0.2.0')] as Set + ) + expect: + reckonStage(inventory, null, null) == '0.1.0' + } + + def 'if target normal claimed, but building insignificant, succeed'() { + given: + def inventory = new VcsInventory( + 'abcdef', + true, + null, + Version.valueOf('0.0.0'), + Version.valueOf('0.0.0'), + 1, + [] as Set, + [Version.valueOf('0.1.0'), Version.valueOf('0.1.1'), Version.valueOf('0.2.0')] as Set + ) + expect: + reckonStage(inventory, null, null) == '0.1.0-beta.0.1+abcdef' + } + + def 'if target normal claimed, and building significant, throw'() { + given: + def inventory = new VcsInventory( + 'abcdef', + true, + null, + Version.valueOf('0.0.0'), + Version.valueOf('0.0.0'), + 1, + [] as Set, + [Version.valueOf('0.1.0'), Version.valueOf('0.1.1'), Version.valueOf('0.2.0')] as Set + ) + when: + reckonStage(inventory, null, 'rc') + then: + thrown(IllegalStateException) + } + + def 'if scope supplier returns invalid scope, throw'() { + given: + def inventory = new VcsInventory( + 'abcdef', + true, + null, + Version.valueOf('1.2.3-milestone.1'), + Version.valueOf('1.2.2'), + 1, + [] as Set, + [] as Set + ) + when: + reckonStage(inventory, 'general', 'beta') + then: + def e = thrown(IllegalArgumentException) + e.getMessage() == 'Scope "general" is not one of: major, minor, patch' + } + + def 'if supplier returns empty, scope defaults to minor if base version is base normal'() { + given: + def inventory = new VcsInventory( + 'abcdef', + true, + null, + Version.valueOf('1.2.2'), + Version.valueOf('1.2.2'), + 1, + [] as Set, + [] as Set + ) + expect: + reckonStage(inventory, null, 'final') == '1.3.0' + } + + def 'if supplier returns empty, scope defaults to scope used by base version'() { + given: + def inventory = new VcsInventory( + 'abcdef', + true, + null, + Version.valueOf('1.2.3-milestone.1'), + Version.valueOf('1.2.2'), + 1, + [] as Set, + [] as Set + ) + expect: + reckonStage(inventory, null, 'final') == '1.2.3' + } + + def 'if no conflict with parallel or claimed, incremented version is returned'() { + given: + def inventory = new VcsInventory( + 'abcdef', + true, + null, + Version.valueOf('1.2.3-milestone.1'), + Version.valueOf('1.2.2'), + 1, + [] as Set, + [] as Set + ) + expect: + reckonStage(inventory, 'major', 'final') == '2.0.0' + } + + def 'if incremented version is in the parallel normals, increment again'() { + given: + def inventory = new VcsInventory( + 'abcdef', + true, + null, + Version.valueOf('1.2.3-milestone.1'), + Version.valueOf('1.2.2'), + 1, + [Version.valueOf('2.0.0')] as Set, + [] as Set + ) + expect: + reckonStage(inventory, 'major', 'final') == '3.0.0' + } + + def 'if target normal is in the claimed versions, throw'() { + given: + def inventory = new VcsInventory( + 'abcdef', + true, + null, + Version.valueOf('1.2.3-milestone.1'), + Version.valueOf('1.2.2'), + 1, + [] as Set, + [Version.valueOf('2.0.0')] as Set + ) + when: + reckonStage(inventory, 'major', 'final') + then: + thrown(IllegalStateException) + } + + def 'if stage supplier returns an invalid stage, throw'() { + given: + VcsInventory inventory = new VcsInventory( + 'abcdef', + true, + null, + Version.valueOf('1.2.3-milestone.2'), + Version.valueOf('1.2.2'), + 5, + [] as Set, + [] as Set + ) + when: + reckonStage(inventory, 'major', 'not') + then: + thrown(IllegalArgumentException) + } + + def 'final stage will return the target normal'() { + given: + VcsInventory inventory = new VcsInventory( + 'abcdef', + true, + null, + Version.valueOf('1.2.3-milestone.2'), + Version.valueOf('1.2.2'), + 5, + [] as Set, + [] as Set + ) + expect: + reckonStage(inventory, 'major', 'final') == '2.0.0' + } + + def 'if target does not contain stage and stage is null, use the default stage and add num commits and commit id'() { + given: + VcsInventory inventory = new VcsInventory( + 'abcdef', + true, + null, + Version.valueOf('1.2.3-milestone.2'), + Version.valueOf('1.2.2'), + 5, + [] as Set, + [] as Set + ) + expect: + reckonStage(inventory, 'major', null) == '2.0.0-beta.0.5+abcdef' + } + + def 'if target does not contain stage and stage is an empty string, use the default stage and add num commits and commit id'() { + given: + VcsInventory inventory = new VcsInventory( + 'abcdef', + true, + null, + Version.valueOf('1.2.3-milestone.2'), + Version.valueOf('1.2.2'), + 5, + [] as Set, + [] as Set + ) + expect: + reckonStage(inventory, 'major', '') == '2.0.0-beta.0.5+abcdef' + } + + def 'if target does not contain stage and stage is present, add num commits and commit id'() { + given: + VcsInventory inventory = new VcsInventory( + 'abcdef', + true, + null, + Version.valueOf('1.2.3-milestone.2'), + Version.valueOf('1.2.2'), + 5, + [] as Set, + [] as Set + ) + expect: + reckonStage(inventory, null, null) == '1.2.3-milestone.2.5+abcdef' + } + + def 'if target contains stage and stage matches, increment'() { + given: + VcsInventory inventory = new VcsInventory( + 'abcdef', + true, + null, + Version.valueOf('1.2.3-milestone.2'), + Version.valueOf('1.2.2'), + 5, + [] as Set, + [] as Set + ) + expect: + reckonStage(inventory, null, 'milestone') == '1.2.3-milestone.3' + } + + def 'if target contains stage and stage differs, start from 1'() { + given: + VcsInventory inventory = new VcsInventory( + 'abcdef', + true, + null, + Version.valueOf('1.2.3-milestone.2'), + Version.valueOf('1.2.2'), + 5, + [] as Set, + [] as Set + ) + expect: + reckonStage(inventory, null, 'rc') == '1.2.3-rc.1' + reckonStage(inventory, 'major', 'rc') == '2.0.0-rc.1' + } + + def 'if repo has no commits, show build metadata as uncommitted'() { + given: + def inventory = new VcsInventory( + null, + false, + null, + Version.valueOf('1.2.3-milestone.2'), + Version.valueOf('1.2.2'), + 5, + [] as Set, + [] as Set + ) + expect: + reckonStage(inventory, null, null) == '1.2.3-milestone.2.5+uncommitted' + + } + + def 'if repo has uncommitted changes, show build metadata as uncommitted'() { + given: + def inventory = new VcsInventory( + 'abcdef', + false, + null, + Version.valueOf('1.2.3-milestone.2'), + Version.valueOf('1.2.2'), + 5, + [] as Set, + [] as Set + ) + expect: + reckonStage(inventory, null, null) == '1.2.3-milestone.2.5+abcdef.uncommitted' + } + + @Unroll + def 'if repo has uncommitted changes, fail when calculating a #stage stage'(String stage) { + given: + def inventory = new VcsInventory( + 'abcdef', + false, + null, + Version.valueOf('1.2.3-milestone.2'), + Version.valueOf('1.2.2'), + 5, + [] as Set, + [] as Set + ) + when: + reckonStage(inventory, null, stage) + then: + thrown(IllegalStateException) + where: + stage << ['rc', 'final'] + } + + def 'if stage supplier returns an invalid stage, throw'() { + given: + def inventory = new VcsInventory( + 'abcdef', + true, + null, + Version.valueOf('1.2.3-milestone.1'), + Version.valueOf('1.2.2'), + 1, + [] as Set, + [] as Set + ) + when: + reckonSnapshot(inventory, 'major', 'not') + then: + thrown(IllegalArgumentException) + } + + def 'if stage is final, return the target normal'() { + given: + def inventory = new VcsInventory( + 'abcdef', + true, + null, + Version.valueOf('1.2.3-milestone.1'), + Version.valueOf('1.2.2'), + 1, + [] as Set, + [] as Set + ) + expect: + reckonSnapshot(inventory, 'major', 'final') == '2.0.0' + } + + def 'if stage is snapshot or null, set pre-release to snapshot'() { + given: + def inventory = new VcsInventory( + 'abcdef', + true, + null, + Version.valueOf('1.2.3-milestone.1'), + Version.valueOf('1.2.2'), + 1, + [] as Set, + [] as Set + ) + expect: + reckonSnapshot(inventory, 'major', 'snapshot') == '2.0.0-SNAPSHOT' + reckonSnapshot(inventory, 'major', null) == '2.0.0-SNAPSHOT' + } + + def 'if repo has uncommitted changes, fail if stage is final'() { + given: + def inventory = new VcsInventory( + 'abcdef', + false, + null, + Version.valueOf('1.2.3-milestone.1'), + Version.valueOf('1.2.2'), + 1, + [] as Set, + [] as Set + ) + when: + reckonSnapshot(inventory, 'major', 'final') + then: + thrown(IllegalStateException) } private String reckonStage(inventory, scope, stage) { - ScopeNormalStrategy normal = new ScopeNormalStrategy({ i -> Optional.ofNullable(scope) }) - StagePreReleaseStrategy preRelease = new StagePreReleaseStrategy(['beta', 'rc', 'final'] as Set, { i, v -> Optional.ofNullable(stage) }) - return Reckoner.reckon({ -> inventory }, normal, preRelease) + return Reckoner.builder() + .vcs { -> inventory } + .scopeCalc { i -> Optional.ofNullable(scope) } + .stages('beta', 'milestone', 'rc', 'final') + .stageCalc { i, v -> Optional.ofNullable(stage) } + .build() + .reckon(); } private String reckonSnapshot(inventory, scope, stage) { - ScopeNormalStrategy normal = new ScopeNormalStrategy({ i -> Optional.ofNullable(scope) }) - SnapshotPreReleaseStrategy preRelease = new SnapshotPreReleaseStrategy({ i, v -> Optional.ofNullable(stage) }) - return Reckoner.reckon({ -> inventory }, normal, preRelease) + return Reckoner.builder() + .vcs { -> inventory } + .scopeCalc { i -> Optional.ofNullable(scope) } + .snapshots() + .stageCalc { i, v -> Optional.ofNullable(stage) } + .build() + .reckon(); } } diff --git a/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/VersionTest.groovy b/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/VersionTest.groovy new file mode 100644 index 00000000..1900d48d --- /dev/null +++ b/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/VersionTest.groovy @@ -0,0 +1,51 @@ +package org.ajoberstar.reckon.core + +import spock.lang.Specification +import spock.lang.Unroll + +class VersionTest extends Specification { + @Unroll + def 'parse returns empty if argument is not valid semantic version'(String version) { + expect: + Version.parse(version) == Optional.empty() + where: + version << [null, 'not a version', '1.0-345'] + } + + def 'parse returns version if argument is valid version'() { + expect: + Version.parse('1.0.0-rc.1').map { it.toString() } == Optional.of('1.0.0-rc.1') + } + + def 'isFinal returns true only if the version does not have a pre-release component'() { + expect: + !Version.valueOf('1.0.0-rc.1').isFinal() + Version.valueOf('1.0.0').isFinal() + } + + def 'getNormal returns the normal component of the version only'() { + expect: + Version.valueOf('1.2.3').getNormal() == Version.valueOf('1.2.3') + Version.valueOf('1.2.3-rc.1').getNormal() == Version.valueOf('1.2.3') + Version.valueOf('1.2.3+other.stuff').getNormal() == Version.valueOf('1.2.3') + } + + def 'incrementNormal correctly uses the scope'() { + given: + def base = Version.valueOf('1.2.3-rc.1') + expect: + base.incrementNormal(Scope.MAJOR) == Version.valueOf('2.0.0') + base.incrementNormal(Scope.MINOR) == Version.valueOf('1.3.0') + base.incrementNormal(Scope.PATCH) == Version.valueOf('1.2.4') + } + + def 'inferScope correctly finds the scope'() { + expect: + Scope.infer(Version.valueOf('1.2.3'), Version.valueOf('1.2.4-milestone.1')) == Optional.of(Scope.PATCH) + Scope.infer(Version.valueOf('1.2.3'), Version.valueOf('1.3.0-milestone.1')) == Optional.of(Scope.MINOR) + Scope.infer(Version.valueOf('1.2.3'), Version.valueOf('2.0.0-milestone.1')) == Optional.of(Scope.MAJOR) + Scope.infer(Version.valueOf('1.2.3'), Version.valueOf('0.4.0')) == Optional.empty() + Scope.infer(Version.valueOf('1.2.3'), Version.valueOf('2.1.0')) == Optional.empty() + Scope.infer(Version.valueOf('1.2.3'), Version.valueOf('1.2.5')) == Optional.empty() + } +} diff --git a/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/VersionsTest.groovy b/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/VersionsTest.groovy deleted file mode 100644 index 56f6defd..00000000 --- a/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/VersionsTest.groovy +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2015-2017 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.ajoberstar.reckon.core - -import com.github.zafarkhaja.semver.Version -import spock.lang.Specification -import spock.lang.Unroll - -class VersionsTest extends Specification { - @Unroll - def 'valueOf returns empty if argument is not valid semantic version'(String version) { - expect: - Versions.valueOf(version) == Optional.empty() - where: - version << [null, 'not a version', '1.0-345'] - } - - def 'valueOf returns version if argument is valid version'() { - expect: - Versions.valueOf('1.0.0-rc.1').map { it.toString() } == Optional.of('1.0.0-rc.1') - } - - def 'isNormal returns true only if the version does not have a pre-release component'() { - expect: - !Versions.isNormal(Version.valueOf('1.0.0-rc.1')) - Versions.isNormal(Version.valueOf('1.0.0')) - } - - def 'getNormal returns the normal component of the version only'() { - expect: - Versions.getNormal(Version.valueOf('1.2.3')) == Version.valueOf('1.2.3') - Versions.getNormal(Version.valueOf('1.2.3-rc.1')) == Version.valueOf('1.2.3') - Versions.getNormal(Version.valueOf('1.2.3+other.stuff')) == Version.valueOf('1.2.3') - } - - def 'incrementNormal correctly uses the scope'() { - expect: - Versions.incrementNormal(Version.valueOf('1.2.3-rc.1'), Scope.MAJOR) == Version.valueOf('2.0.0') - Versions.incrementNormal(Version.valueOf('1.2.3-rc.1'), Scope.MINOR) == Version.valueOf('1.3.0') - Versions.incrementNormal(Version.valueOf('1.2.3-rc.1'), Scope.PATCH) == Version.valueOf('1.2.4') - } - - def 'inferScope correctly finds the scope'() { - expect: - Versions.inferScope(Version.valueOf('1.2.3'), Version.valueOf('1.2.4-milestone.1')) == Optional.of(Scope.PATCH) - Versions.inferScope(Version.valueOf('1.2.3'), Version.valueOf('1.3.0-milestone.1')) == Optional.of(Scope.MINOR) - Versions.inferScope(Version.valueOf('1.2.3'), Version.valueOf('2.0.0-milestone.1')) == Optional.of(Scope.MAJOR) - Versions.inferScope(Version.valueOf('1.2.3'), Version.valueOf('0.4.0')) == Optional.empty() - Versions.inferScope(Version.valueOf('1.2.3'), Version.valueOf('2.1.0')) == Optional.empty() - Versions.inferScope(Version.valueOf('1.2.3'), Version.valueOf('1.2.5')) == Optional.empty() - } -} diff --git a/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/strategy/ScopeNormalStrategyTest.groovy b/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/strategy/ScopeNormalStrategyTest.groovy deleted file mode 100644 index 1b662514..00000000 --- a/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/strategy/ScopeNormalStrategyTest.groovy +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2015-2017 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.ajoberstar.reckon.core.strategy - -import com.github.zafarkhaja.semver.Version -import org.ajoberstar.reckon.core.Scope -import org.ajoberstar.reckon.core.VcsInventory -import spock.lang.Specification - -class ScopeNormalStrategyTest extends Specification { - def 'if scope supplier returns null, throw'() { - given: - def inventory = new VcsInventory( - 'abcdef', - true, - null, - Version.valueOf('1.2.3-milestone.1'), - Version.valueOf('1.2.2'), - 1, - [] as Set, - [] as Set - ) - when: - new ScopeNormalStrategy({ null }).reckonNormal(inventory) - then: - thrown(NullPointerException) - } - - def 'if scope supplier returns invalid scope, throw'() { - given: - def inventory = new VcsInventory( - 'abcdef', - true, - null, - Version.valueOf('1.2.3-milestone.1'), - Version.valueOf('1.2.2'), - 1, - [] as Set, - [] as Set - ) - when: - new ScopeNormalStrategy({ Optional.of("general") }).reckonNormal(inventory) - then: - def e = thrown(IllegalArgumentException) - e.getMessage() == 'Scope "general" is not one of: major, minor, patch' - } - - def 'if supplier returns empty, scope defaults to minor if base version is base normal'() { - given: - def inventory = new VcsInventory( - 'abcdef', - true, - null, - Version.valueOf('1.2.2'), - Version.valueOf('1.2.2'), - 1, - [] as Set, - [] as Set - ) - expect: - new ScopeNormalStrategy({ Optional.empty() }).reckonNormal(inventory).toString() == '1.3.0' - } - - def 'if supplier returns empty, scope defaults to scope used by base version'() { - given: - def inventory = new VcsInventory( - 'abcdef', - true, - null, - Version.valueOf('1.2.3-milestone.1'), - Version.valueOf('1.2.2'), - 1, - [] as Set, - [] as Set - ) - expect: - new ScopeNormalStrategy({ Optional.empty() }).reckonNormal(inventory).toString() == '1.2.3' - } - - def 'if no conflict with parallel or claimed, incremented version is returned'() { - given: - def inventory = new VcsInventory( - 'abcdef', - true, - null, - Version.valueOf('1.2.3-milestone.1'), - Version.valueOf('1.2.2'), - 1, - [] as Set, - [] as Set - ) - expect: - new ScopeNormalStrategy({ Optional.of('major') }).reckonNormal(inventory).toString() == '2.0.0' - } - - def 'if incremented version is in the parallel normals, increment again'() { - given: - def inventory = new VcsInventory( - 'abcdef', - true, - null, - Version.valueOf('1.2.3-milestone.1'), - Version.valueOf('1.2.2'), - 1, - [Version.valueOf('2.0.0')] as Set, - [] as Set - ) - expect: - new ScopeNormalStrategy({ Optional.of('major') }).reckonNormal(inventory).toString() == '3.0.0' - } - - def 'if target normal is in the claimed versions, throw'() { - given: - def inventory = new VcsInventory( - 'abcdef', - true, - null, - Version.valueOf('1.2.3-milestone.1'), - Version.valueOf('1.2.2'), - 1, - [] as Set, - [Version.valueOf('2.0.0')] as Set - ) - when: - new ScopeNormalStrategy({ Optional.of('major') }).reckonNormal(inventory) - then: - thrown(IllegalStateException) - } - -} diff --git a/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/strategy/SnapshotPreReleaseStrategyTest.groovy b/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/strategy/SnapshotPreReleaseStrategyTest.groovy deleted file mode 100644 index 42f04114..00000000 --- a/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/strategy/SnapshotPreReleaseStrategyTest.groovy +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2015-2017 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.ajoberstar.reckon.core.strategy - -import com.github.zafarkhaja.semver.Version -import org.ajoberstar.reckon.core.VcsInventory -import spock.lang.Specification - -class SnapshotPreReleaseStrategyTest extends Specification { - def 'if stage supplier returns an invalid stage, throw'() { - given: - def inventory = new VcsInventory( - 'abcdef', - true, - null, - Version.valueOf('1.2.3-milestone.1'), - Version.valueOf('1.2.2'), - 1, - [] as Set, - [] as Set - ) - when: - strategy('not').reckonTargetVersion(inventory, Version.valueOf('2.0.0')) - then: - thrown(IllegalArgumentException) - } - - def 'if stage is final, return the target normal'() { - given: - def inventory = new VcsInventory( - 'abcdef', - true, - null, - Version.valueOf('1.2.3-milestone.1'), - Version.valueOf('1.2.2'), - 1, - [] as Set, - [] as Set - ) - expect: - strategy('final').reckonTargetVersion(inventory, Version.valueOf('2.0.0')).toString() == '2.0.0' - } - - def 'if stage is snapshot or null, set pre-release to snapshot'() { - given: - def inventory = new VcsInventory( - 'abcdef', - true, - null, - Version.valueOf('1.2.3-milestone.1'), - Version.valueOf('1.2.2'), - 1, - [] as Set, - [] as Set - ) - expect: - strategy('snapshot').reckonTargetVersion(inventory, Version.valueOf('2.0.0')).toString() == '2.0.0-SNAPSHOT' - strategy(null).reckonTargetVersion(inventory, Version.valueOf('2.0.0')).toString() == '2.0.0-SNAPSHOT' - } - - def 'if repo has uncommitted changes, fail if stage is final'() { - given: - def inventory = new VcsInventory( - 'abcdef', - false, - null, - Version.valueOf('1.2.3-milestone.1'), - Version.valueOf('1.2.2'), - 1, - [] as Set, - [] as Set - ) - when: - strategy('final').reckonTargetVersion(inventory, Version.valueOf('2.0.0')) - then: - thrown(IllegalStateException) - } - - private SnapshotPreReleaseStrategy strategy(stage) { - return new SnapshotPreReleaseStrategy({ i, v -> Optional.ofNullable(stage) }) - } -} diff --git a/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/strategy/StagePreReleaseStrategyTest.groovy b/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/strategy/StagePreReleaseStrategyTest.groovy deleted file mode 100644 index ebfa3dc4..00000000 --- a/reckon-core/src/test/groovy/org/ajoberstar/reckon/core/strategy/StagePreReleaseStrategyTest.groovy +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2015-2017 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.ajoberstar.reckon.core.strategy - -import com.github.zafarkhaja.semver.Version -import org.ajoberstar.reckon.core.VcsInventory -import spock.lang.Specification -import spock.lang.Unroll - -class StagePreReleaseStrategyTest extends Specification { - VcsInventory inventory = new VcsInventory( - 'abcdef', - true, - null, - Version.valueOf('1.2.3-milestone.2'), - Version.valueOf('1.2.2'), - 5, - [] as Set, - [] as Set - ) - - def 'if stage supplier returns an invalid stage, throw'() { - when: - strategy('not').reckonTargetVersion(inventory, Version.valueOf('2.0.0')) - then: - thrown(IllegalArgumentException) - } - - def 'final stage will return the target normal'() { - expect: - strategy('final').reckonTargetVersion(inventory, Version.valueOf('2.0.0')).toString() == '2.0.0' - } - - def 'if target does not contain stage and stage is null, use the default stage and add num commits and commit id'() { - expect: - strategy(null).reckonTargetVersion(inventory, Version.valueOf('2.0.0')).toString() == '2.0.0-initial.0.5+abcdef' - } - - def 'if target does not contain stage and stage is an empty string, use the default stage and add num commits and commit id'() { - expect: - strategy('').reckonTargetVersion(inventory, Version.valueOf('2.0.0')).toString() == '2.0.0-initial.0.5+abcdef' - } - - def 'if target does not contain stage and stage is present, add num commits and commit id'() { - expect: - strategy(null).reckonTargetVersion(inventory, Version.valueOf('1.2.3')).toString() == '1.2.3-milestone.2.5+abcdef' - } - - def 'if target contains stage and stage matches, increment'() { - expect: - strategy('milestone').reckonTargetVersion(inventory, Version.valueOf('1.2.3')).toString() == '1.2.3-milestone.3' - } - - def 'if target contains stage and stage differs, start from 1'() { - expect: - strategy('rc').reckonTargetVersion(inventory, Version.valueOf('1.2.3')).toString() == '1.2.3-rc.1' - strategy('rc').reckonTargetVersion(inventory, Version.valueOf('2.0.0')).toString() == '2.0.0-rc.1' - } - - def 'if repo has no commits, show build metadata as uncommitted'() { - given: - def inventoryUncommitted = new VcsInventory( - null, - false, - null, - Version.valueOf('1.2.3-milestone.2'), - Version.valueOf('1.2.2'), - 5, - [] as Set, - [] as Set - ) - expect: - strategy(null).reckonTargetVersion(inventoryUncommitted, Version.valueOf('1.2.3')).toString() == '1.2.3-milestone.2.5+uncommitted' - - } - - def 'if repo has uncommitted changes, show build metadata as uncommitted'() { - given: - def inventoryUncommitted = new VcsInventory( - 'abcdef', - false, - null, - Version.valueOf('1.2.3-milestone.2'), - Version.valueOf('1.2.2'), - 5, - [] as Set, - [] as Set - ) - expect: - strategy(null).reckonTargetVersion(inventoryUncommitted, Version.valueOf('1.2.3')).toString() == '1.2.3-milestone.2.5+abcdef.uncommitted' - } - - @Unroll - def 'if repo has uncommitted changes, fail when calculating a #stage stage'(String stage) { - given: - def inventoryUncommitted = new VcsInventory( - 'abcdef', - false, - null, - Version.valueOf('1.2.3-milestone.2'), - Version.valueOf('1.2.2'), - 5, - [] as Set, - [] as Set - ) - when: - strategy(stage).reckonTargetVersion(inventoryUncommitted, Version.valueOf('1.2.3')) - then: - thrown(IllegalStateException) - where: - stage << ['rc', 'final'] - } - - private StagePreReleaseStrategy strategy(String stage) { - return new StagePreReleaseStrategy(['initial', 'milestone', 'rc', 'final'] as Set, { i, v -> Optional.ofNullable(stage) }) - } -} diff --git a/reckon-gradle/src/compatTest/groovy/org/ajoberstar/reckon/gradle/BaseCompatTest.groovy b/reckon-gradle/src/compatTest/groovy/org/ajoberstar/reckon/gradle/BaseCompatTest.groovy index 3eb5c099..e606a3d1 100644 --- a/reckon-gradle/src/compatTest/groovy/org/ajoberstar/reckon/gradle/BaseCompatTest.groovy +++ b/reckon-gradle/src/compatTest/groovy/org/ajoberstar/reckon/gradle/BaseCompatTest.groovy @@ -33,14 +33,18 @@ class BaseCompatTest extends Specification { remote.commit(message: 'second commit') } - def 'if no git repo found, version is unspecified'() { + def 'if no git repo found, version is defaulted'() { given: buildFile << """ plugins { - id 'org.ajoberstar.grgit' id 'org.ajoberstar.reckon' } +reckon { + scopeFromProp() + stageFromProp('alpha','beta', 'final') +} + task printVersion { doLast { println project.version @@ -50,7 +54,7 @@ task printVersion { when: def result = build('printVersion', '-q') then: - result.output.normalize() == 'No git repository found for :. Accessing grgit will cause an NPE.\nunspecified\n' + result.output.normalize() == 'No git repository found for :. Accessing grgit will cause an NPE.\n0.1.0-alpha.0.0+uncommitted\n' } def 'if no strategies specified, build fails'() { @@ -59,7 +63,6 @@ task printVersion { buildFile << """ plugins { - id 'org.ajoberstar.grgit' id 'org.ajoberstar.reckon' } @@ -72,7 +75,7 @@ task printVersion { when: def result = buildAndFail('printVersion') then: - result.output.contains('Must provide strategies for normal and preRelease on the reckon extension.') + result.output.contains('Must provide a scope supplier.') } def 'if reckoned version has build metadata no tag created'() { @@ -81,13 +84,12 @@ task printVersion { buildFile << """ plugins { - id 'org.ajoberstar.grgit' id 'org.ajoberstar.reckon' } reckon { - normal = scopeFromProp() - preRelease = stageFromProp('alpha','beta', 'final') + scopeFromProp() + stageFromProp('alpha','beta', 'final') } """ local.add(patterns: ['build.gradle']) @@ -100,19 +102,42 @@ reckon { result.task(':reckonTagPush').outcome == TaskOutcome.SKIPPED } - def 'if reckoned version has no build metadata tag created and pushed'() { + def 'if reckoned version is SNAPSHOT no tag created'() { given: def local = Grgit.clone(dir: projectDir, uri: remote.repository.rootDir) buildFile << """ plugins { - id 'org.ajoberstar.grgit' id 'org.ajoberstar.reckon' } reckon { - normal = scopeFromProp() - preRelease = stageFromProp('alpha','beta', 'final') + scopeFromProp() + snapshotFromProp() +} +""" + local.add(patterns: ['build.gradle']) + local.commit(message: 'Build file') + when: + def result = build('reckonTagPush') + then: + result.output.contains('Reckoned version: 1.1.0-SNAPSHOT') + result.task(':reckonTagCreate').outcome == TaskOutcome.SKIPPED + result.task(':reckonTagPush').outcome == TaskOutcome.SKIPPED + } + + def 'if reckoned version is significant tag created and pushed'() { + given: + def local = Grgit.clone(dir: projectDir, uri: remote.repository.rootDir) + + buildFile << """ +plugins { + id 'org.ajoberstar.reckon' +} + +reckon { + scopeFromProp() + stageFromProp('alpha','beta', 'final') } """ local.add(patterns: ['build.gradle']) @@ -134,13 +159,12 @@ reckon { buildFile << """ plugins { - id 'org.ajoberstar.grgit' id 'org.ajoberstar.reckon' } reckon { - normal = scopeFromProp() - preRelease = stageFromProp('alpha','beta', 'final') + scopeFromProp() + stageFromProp('alpha', 'beta', 'final') } """ local.add(patterns: ['build.gradle']) @@ -154,6 +178,34 @@ reckon { result.task(':reckonTagPush').outcome == TaskOutcome.SKIPPED } + def 'old syntax of extension does not fail'() { + given: + def local = Grgit.clone(dir: projectDir, uri: remote.repository.rootDir) + + buildFile << """ +plugins { + id 'org.ajoberstar.reckon' +} + +reckon { + normal = scopeFromProp() + preRelease = stageFromProp('alpha','beta', 'final') +} + +task printVersion { + doLast { + println project.version + } +} +""" + local.add(patterns: ['build.gradle']) + local.commit(message: 'Build file') + when: + def result = build('printVersion') + then: + result.output.contains('Reckoned version: 1.1.0-alpha.0') + } + private BuildResult build(String... args = []) { return GradleRunner.create() .withGradleVersion(System.properties['compat.gradle.version']) diff --git a/reckon-gradle/src/main/java/org/ajoberstar/reckon/gradle/ReckonExtension.java b/reckon-gradle/src/main/java/org/ajoberstar/reckon/gradle/ReckonExtension.java index faf70d3f..8c64ebd7 100644 --- a/reckon-gradle/src/main/java/org/ajoberstar/reckon/gradle/ReckonExtension.java +++ b/reckon-gradle/src/main/java/org/ajoberstar/reckon/gradle/ReckonExtension.java @@ -1,23 +1,12 @@ package org.ajoberstar.reckon.gradle; -import java.util.Arrays; import java.util.Optional; -import java.util.Set; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.stream.Collectors; -import com.github.zafarkhaja.semver.Version; import org.ajoberstar.grgit.Grgit; -import org.ajoberstar.reckon.core.NormalStrategy; -import org.ajoberstar.reckon.core.PreReleaseStrategy; +import org.ajoberstar.grgit.Repository; import org.ajoberstar.reckon.core.Reckoner; -import org.ajoberstar.reckon.core.VcsInventory; -import org.ajoberstar.reckon.core.VcsInventorySupplier; -import org.ajoberstar.reckon.core.git.GitInventorySupplier; -import org.ajoberstar.reckon.core.strategy.ScopeNormalStrategy; -import org.ajoberstar.reckon.core.strategy.SnapshotPreReleaseStrategy; -import org.ajoberstar.reckon.core.strategy.StagePreReleaseStrategy; +import org.ajoberstar.reckon.core.Version; +import org.eclipse.jgit.api.Git; import org.gradle.api.Project; public class ReckonExtension { @@ -26,45 +15,45 @@ public class ReckonExtension { private static final String SNAPSHOT_PROP = "reckon.snapshot"; private Project project; - private Grgit grgit; - private VcsInventorySupplier vcsInventory; - private NormalStrategy normal; - private PreReleaseStrategy preRelease; + private Reckoner.Builder reckoner; - public ReckonExtension(Project project) { + public ReckonExtension(Project project, Grgit grgit) { this.project = project; + this.reckoner = Reckoner.builder(); + org.eclipse.jgit.lib.Repository repo = Optional.ofNullable(grgit) + .map(Grgit::getRepository) + .map(Repository::getJgit) + .map(Git::getRepository) + .orElse(null); + this.reckoner.git(repo); } - public void setVcsInventory(VcsInventorySupplier vcsInventory) { - this.vcsInventory = vcsInventory; + @Deprecated + public void setNormal(ReckonExtension ext) { + project.getLogger().warn("reckon.normal = scopeFromProp() is deprecated and will be removed in 1.0.0. Call reckon.scopeFromProp() instead."); + // no op } - public void setNormal(NormalStrategy normal) { - this.normal = normal; + @Deprecated + public void setPreRelease(ReckonExtension ext) { + project.getLogger().warn("reckon.preRelease = stageFromProp() or snapshotFromProp() is deprecated and will be removed in 1.0.0. Call reckon.stageFromProp() or reckon.snapshotFromProp() instead."); + // no op } - public void setPreRelease(PreReleaseStrategy preRelease) { - this.preRelease = preRelease; + public ReckonExtension scopeFromProp() { + this.reckoner.scopeCalc(inventory -> findProperty(SCOPE_PROP)); + return this; } - public VcsInventorySupplier git(Grgit grgit) { - this.grgit = grgit; - return new GitInventorySupplier(grgit.getRepository().getJgit().getRepository()); + public ReckonExtension stageFromProp(String... stages) { + this.reckoner.stages(stages); + this.reckoner.stageCalc((inventory, targetNormal) -> findProperty(STAGE_PROP)); + return this; } - public NormalStrategy scopeFromProp() { - Function> supplier = inventory -> findProperty(SCOPE_PROP); - return new ScopeNormalStrategy(supplier); - } - - public PreReleaseStrategy stageFromProp(String... stages) { - Set stageSet = Arrays.stream(stages).collect(Collectors.toSet()); - BiFunction> supplier = (inventory, targetNormal) -> findProperty(STAGE_PROP); - return new StagePreReleaseStrategy(stageSet, supplier); - } - - public PreReleaseStrategy snapshotFromProp() { - BiFunction> supplier = (inventory, targetNormal) -> { + public ReckonExtension snapshotFromProp() { + this.reckoner.snapshots(); + this.reckoner.stageCalc((inventory, targetNormal) -> { Optional stageProp = findProperty(STAGE_PROP); Optional snapshotProp = findProperty(SNAPSHOT_PROP) .map(Boolean::parseBoolean) @@ -75,8 +64,8 @@ public PreReleaseStrategy snapshotFromProp() { }); return stageProp.isPresent() ? stageProp : snapshotProp; - }; - return new SnapshotPreReleaseStrategy(supplier); + }); + return this; } private Optional findProperty(String name) { @@ -86,20 +75,9 @@ private Optional findProperty(String name) { .map(Object::toString); } - Grgit getGrgit() { - return grgit; - } - - String reckonVersion() { - if (vcsInventory == null) { - project.getLogger().warn("No VCS found/configured. Version will be 'unspecified'."); - return "unspecified"; - } else if (normal == null || preRelease == null) { - throw new IllegalStateException("Must provide strategies for normal and preRelease on the reckon extension."); - } else { - String version = Reckoner.reckon(vcsInventory, normal, preRelease); - project.getLogger().warn("Reckoned version: {}", version); - return version; - } + Version reckonVersion() { + Version version = reckoner.build().reckon(); + project.getLogger().warn("Reckoned version: {}", version); + return version; } } diff --git a/reckon-gradle/src/main/java/org/ajoberstar/reckon/gradle/ReckonPlugin.java b/reckon-gradle/src/main/java/org/ajoberstar/reckon/gradle/ReckonPlugin.java index 0fce6e55..3fee0c28 100644 --- a/reckon-gradle/src/main/java/org/ajoberstar/reckon/gradle/ReckonPlugin.java +++ b/reckon-gradle/src/main/java/org/ajoberstar/reckon/gradle/ReckonPlugin.java @@ -7,6 +7,7 @@ import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import org.ajoberstar.grgit.Grgit; +import org.ajoberstar.reckon.core.Version; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.Task; @@ -20,48 +21,44 @@ public void apply(Project project) { if (!project.equals(project.getRootProject())) { throw new IllegalStateException("org.ajoberstar.reckon can only be applied to the root project."); } - ReckonExtension extension = project.getExtensions().create("reckon", ReckonExtension.class, project); + project.getPluginManager().apply("org.ajoberstar.grgit"); - project.getPluginManager().withPlugin("org.ajoberstar.grgit", plugin -> { - Grgit grgit = (Grgit) project.findProperty("grgit"); - if (grgit != null) { - extension.setVcsInventory(extension.git(grgit)); - } - }); + Grgit grgit = (Grgit) project.findProperty("grgit"); + ReckonExtension extension = project.getExtensions().create("reckon", ReckonExtension.class, project, grgit); DelayedVersion sharedVersion = new DelayedVersion(extension::reckonVersion); project.allprojects(prj -> { prj.setVersion(sharedVersion); }); - Task tag = createTagTask(project, extension); - Task push = createPushTask(project, extension, tag); + Task tag = createTagTask(project, extension, grgit); + Task push = createPushTask(project, extension, grgit, tag); push.dependsOn(tag); } - private Task createTagTask(Project project, ReckonExtension extension) { + private Task createTagTask(Project project, ReckonExtension extension, Grgit grgit) { Task task = project.getTasks().create(TAG_TASK); task.setDescription("Tag version inferred by reckon."); task.setGroup("publishing"); task.onlyIf(t -> { - String version = project.getVersion().toString(); - // using the presence of build metadata as the indicator of taggable versions - boolean insignificant = version.contains("+"); + Version version = ((DelayedVersion) project.getVersion()).getVersion(); + // rebuilds shouldn't trigger a new tag - boolean alreadyTagged = extension.getGrgit().getTag().list().stream() - .anyMatch(tag -> tag.getName().equals(version)); - return !(insignificant || alreadyTagged); + boolean alreadyTagged = grgit.getTag().list().stream() + .anyMatch(tag -> tag.getName().equals(version.toString())); + + return version.isSignificant() && !alreadyTagged; }); task.doLast(t -> { Map args = new HashMap<>(); args.put("name", project.getVersion()); args.put("message", project.getVersion()); - extension.getGrgit().getTag().add(args); + grgit.getTag().add(args); }); return task; } - private Task createPushTask(Project project, ReckonExtension extension, Task create) { + private Task createPushTask(Project project, ReckonExtension extension, Grgit grgit, Task create) { Task task = project.getTasks().create(PUSH_TASK); task.setDescription("Push version tag created by reckon."); task.setGroup("publishing"); @@ -69,21 +66,25 @@ private Task createPushTask(Project project, ReckonExtension extension, Task cre task.doLast(t -> { Map args = new HashMap<>(); args.put("refsOrSpecs", Arrays.asList("refs/tags/" + project.getVersion().toString())); - extension.getGrgit().push(args); + grgit.push(args); }); return task; } private static class DelayedVersion { - private final Supplier reckoner; + private final Supplier reckoner; - public DelayedVersion(Supplier reckoner) { + public DelayedVersion(Supplier reckoner) { this.reckoner = Suppliers.memoize(reckoner); } + public Version getVersion() { + return reckoner.get(); + } + @Override public String toString() { - return reckoner.get(); + return reckoner.get().toString(); } } }