Skip to content

Commit

Permalink
Update SemverRelease to support skipping a release from commit (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
serpro69 committed Apr 28, 2024
1 parent 22a359c commit 5799723
Show file tree
Hide file tree
Showing 13 changed files with 142 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ class SemverRelease : AutoCloseable {
* THEN that increment will be returned based on the precedence rules,
* ELSE [Increment.NONE] is returned.
*
* IF any of the commits contain a keyword for skipping a release, as configured by [GitMessageConfig.skip],
* only a subset of the commits (from `HEAD` until the first commit containing "skip" keyword)
* will be used to calculate the next increment.
*
* IF the repository `HEAD` points to the [Repository.latestVersionTag],
* OR the repository `HEAD` points to any other existing release tag, as configured by [GitTagConfig.prefix],
* THEN [Increment.NONE] is returned.
Expand All @@ -263,7 +267,10 @@ class SemverRelease : AutoCloseable {
val isHeadOnTag by lazy { repo.tags(prefix).any { head == it.peeledObjectId || head == it.objectId } }
return when {
head == tagObjectId || isHeadOnTag -> Increment.NONE
else -> repo.log(untilTag = latestTag, tagPrefix = prefix).nextIncrement()
else -> repo.log(untilTag = latestTag, tagPrefix = prefix)
// take commits until [skip] keyword is encountered, drop the rest
.takeWhile { !it.message.full().contains(config.git.message.skip) }
.nextIncrement()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ abstract class ConfigurationProvider : Configuration {
override val minor: String = propertyOrNull("git.message.minor") ?: super.minor
override val patch: String = propertyOrNull("git.message.patch") ?: super.patch
override val preRelease: String = propertyOrNull("git.message.preRelease") ?: super.preRelease
override val skip: String = propertyOrNull("git.message.skip") ?: super.skip
override val ignoreCase: Boolean = propertyOrNull("git.message.ignoreCase") ?: super.ignoreCase
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ class PojoGitMessageConfig internal constructor() : GitMessageConfig {
override var minor: String = super.minor
override var patch: String = super.patch
override var preRelease: String = super.preRelease
override var skip: String = super.skip
override var ignoreCase: Boolean = super.ignoreCase
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface GitMessageConfig {
val minor: String get() = "[minor]"
val patch: String get() = "[patch]"
val preRelease: String get() = "[pre release]"
val skip: String get() = "[skip]"
val ignoreCase: Boolean get() = false

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ package io.github.serpro69.semverkt.release
import io.github.serpro69.semverkt.release.configuration.PropertiesConfiguration
import io.github.serpro69.semverkt.release.repo.GitRepository
import io.github.serpro69.semverkt.spec.Semver
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import org.eclipse.jgit.api.Git
import java.nio.file.Files
import java.util.*
import java.util.Properties

class SemverReleaseTest : TestFixtures({ test ->

Expand Down Expand Up @@ -472,5 +471,35 @@ class SemverReleaseTest : TestFixtures({ test ->
}
}
}

describe(testName("nextIncrement when skipping a release")) {
it("should return PATCH when [skip] follows a higher-precedence keyword") {
git().use {
it.addCommit("Release [major]")
it.addCommit("Release [pre release]")
it.addCommit("Release [minor]")
it.addCommit("[skip] release")
it.addCommit("Release [patch]")
}
test.semverRelease(repo).use {
it.nextIncrement() shouldBe Increment.PATCH
it.nextIncrement(submodule) shouldBe Increment.PATCH
}
}
it("should return DEFAULT when [skip] is last keyword") {
git().use {
it.addCommit("Release [major]")
it.addCommit("Release [pre release]")
it.addCommit("Release [minor]")
it.addCommit("Release [patch]")
it.addCommit("[skip] release")
it.addCommit("No release here")
}
test.semverRelease(repo).use {
it.nextIncrement() shouldBe Increment.DEFAULT
it.nextIncrement(submodule) shouldBe Increment.DEFAULT
}
}
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class DefaultConfigurationTest : DescribeSpec({
dc.git.message.minor shouldBe provider.git.message.minor
dc.git.message.patch shouldBe provider.git.message.patch
dc.git.message.preRelease shouldBe provider.git.message.preRelease
dc.git.message.skip shouldBe provider.git.message.skip
dc.git.message.ignoreCase shouldBe provider.git.message.ignoreCase
// version
dc.version.initialVersion shouldBe provider.version.initialVersion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class DslConfigurationTest : DescribeSpec({
c.git.repo.remoteName shouldBe "origin"
c.git.repo.cleanRule shouldBe CleanRule.TRACKED
c.git.tag.useBranches shouldBe false
c.git.message.skip shouldBe "[skip]"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class JsonConfigurationTest : DescribeSpec({
"minor": "[min]",
"patch": "[pat]",
"preRelease": "[pre]",
"skip": "[sk]",
"ignoreCase": true
}
},
Expand Down Expand Up @@ -88,6 +89,7 @@ class JsonConfigurationTest : DescribeSpec({
jc.git.message.minor shouldBe "[min]"
jc.git.message.patch shouldBe "[pat]"
jc.git.message.preRelease shouldBe "[pre]"
jc.git.message.skip shouldBe "[sk]"
jc.git.message.ignoreCase shouldBe true

// version properties
Expand Down Expand Up @@ -133,6 +135,7 @@ class JsonConfigurationTest : DescribeSpec({
jc.git.message.minor shouldBe DefaultConfiguration.git.message.minor
jc.git.message.patch shouldBe DefaultConfiguration.git.message.patch
jc.git.message.preRelease shouldBe DefaultConfiguration.git.message.preRelease
jc.git.message.skip shouldBe DefaultConfiguration.git.message.skip
jc.git.message.ignoreCase shouldBe DefaultConfiguration.git.message.ignoreCase
// version
jc.version.initialVersion shouldBe DefaultConfiguration.version.initialVersion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class PropertiesConfigurationTest : DescribeSpec({
pc.git.message.minor shouldBe DefaultConfiguration.git.message.minor
pc.git.message.patch shouldBe DefaultConfiguration.git.message.patch
pc.git.message.preRelease shouldBe DefaultConfiguration.git.message.preRelease
pc.git.message.skip shouldBe DefaultConfiguration.git.message.skip
pc.git.message.ignoreCase shouldBe DefaultConfiguration.git.message.ignoreCase
// version
pc.version.initialVersion shouldBe DefaultConfiguration.version.initialVersion
Expand Down
13 changes: 13 additions & 0 deletions semantic-versioning/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ Version precedence follows semver rules (`[major]` -> `[minor]` -> `[patch]` ->

By default, releasing with uncommitted changes is not allowed and will fail the `:tag` task. This can be overridden by setting `git.repo.cleanRule` configuration property to `none`. The default rule will only consider tracked changes, and will allow releases with untracked files in the repository. To override this and only allow releases of "clean" repository, set the `cleanRule` property to `all` (See [Configuration](#configuration) section for more details.)

#### Skipping a release commit

There could be situations where a "release keyword" was added to a commit unintentionally, or a release with a given past commit is otherwise unwanted anymore. In this case the plugin supports "skipping" the past commits from the next release calculation via `"[skip]"` keyword in the commit message (Can be configured via `git.message.skip` configuration property.)

If a commit with the "skip" keyword is found between `HEAD` and the latest version when calculating the next release, only commits until the "skip commit" will be used to determine the next version.

> [!NOTE]
> This only applies when releasing the next version from a commit message. Using `-Pincrement` gradle property will override this behavior as it takes precedence over commit messages.
#### Releasing via gradle properties

Instead of having the plugin look up keywords in commits, one can use `-Pincrement` property instead with the `:tag` task.
Expand Down Expand Up @@ -263,6 +272,10 @@ version=0.0.0

Any module that does not have changes will not get the new version applied to it, and hence will stay on version `0.0.0` throughout the build process runtime (barring some external modifications to the `version` property), and hence this can be used in conditional checks to skip certain tasks for a given module.

> [!NOTE]
> Using the aforementioned "version placeholder" concept is mostly useful in the context of [single-tag monorepo](#single-tag-monorepo) project, because we can't determine the latest version of a given module from a single git tag.
> With the [multi-tag monorepo](#multi-tag-monorepo) project, each configured submodule will have a real version assigned to it based on its own git tag.
This comes with some downsides which are good to be aware of when considering to version each submodule separately:

- the whole project is still versioned in git via tags and according to semver rules, however (configured) submodules are versioned individually
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ import io.github.serpro69.semverkt.gradle.plugin.util.UpToDate
import io.github.serpro69.semverkt.release.Increment
import io.github.serpro69.semverkt.spec.Semver
import io.kotest.common.ExperimentalKotest
import io.kotest.core.names.TestName
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.core.spec.style.scopes.DescribeSpecContainerScope
import io.kotest.core.spec.style.scopes.addContainer
import org.eclipse.jgit.api.Git
import org.gradle.testkit.runner.TaskOutcome
import kotlin.io.path.createDirectories
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,44 @@ class SemverKtPluginFT : AbstractFT({ t ->
false
)
}

it("should update to next PATCH when [skip] follows [$keyword] keyword") {
val project = SemverKtTestProject()
// Arrange
Git.open(project.projectDir.toFile()).use {
it.tag().setName("v0.1.0").call() // set initial version
project.projectDir.resolve("text.txt").createFile().writeText("Hello")
it.add().addFilepattern("text.txt").call()
it.commit().setMessage("New commit\n\n[$keyword]").call()
it.commit().setMessage("[skip] release").call()
it.commit().setMessage("Release \n\n[patch]").call()
}
// Act
val result = fromCommit.tag(project)(r)(dryRun)
// Assert
result.task(":tag")?.outcome shouldBe t.expectedOutcome(dryRun, UpToDate.FALSE)
result.output shouldContain t.expectedConfigure(Semver("0.1.1"), emptyList())
result.output shouldContain t.expectedTag(project.name, Semver("0.1.1"), dryRun, UpToDate.FALSE, false)
}

it("should not create a new tag when [skip] is the last keyword") {
val project = SemverKtTestProject()
// Arrange
Git.open(project.projectDir.toFile()).use {
it.tag().setName("v0.1.0").call() // set initial version
project.projectDir.resolve("text.txt").createFile().writeText("Hello")
it.add().addFilepattern("text.txt").call()
it.commit().setMessage("New commit\n\n[$keyword]").call()
it.commit().setMessage("[skip] release").call()
it.commit().setMessage("New commit w/o releases").call()
}
// Act
val result = fromCommit.tag(project)(r)(dryRun)
// Assert
result.task(":tag")?.outcome shouldBe t.expectedOutcome(dryRun, UpToDate.TRUE)
result.output shouldContain t.expectedConfigure(Semver("0.0.0"), emptyList())
result.output shouldContain t.expectedTag(project.name, null, dryRun, UpToDate.TRUE, false)
}
}
}

Expand All @@ -102,27 +140,53 @@ class SemverKtPluginFT : AbstractFT({ t ->
)
}

it("should take precedence with -Prelease -Pincrement=$inc over commit message with [major] keyword") {
listOf("major", "skip").forEach { kw ->
it("should take precedence with -Prelease -Pincrement=$inc over commit message with [$kw] keyword") {
val project = SemverKtTestProject()
// Arrange
Git.open(project.projectDir.toFile()).use {
it.tag().setName("v0.1.0").call() // set initial version
project.projectDir.resolve("text.txt").createFile().writeText("Hello")
it.add().addFilepattern("text.txt").call()
// set to MAJOR or SKIP to check if lower precedence values will override
it.commit().setMessage("New commit\n\n[$kw]").call()
}
// Act
val result = fromProperty.tag(project)(r)(dryRun)
// Assert
result.task(":tag")?.outcome shouldBe t.expectedOutcome(dryRun, UpToDate.FALSE)
val nextVer = when (inc) {
"major" -> "1.0.0"
"minor" -> "0.2.0"
"patch" -> "0.1.1"
"pre_release" -> "0.2.0-rc.1"
else -> "42" // shouldn't really get here
}
result.output shouldContain t.expectedConfigure(Semver(nextVer), emptyList())
result.output shouldContain t.expectedTag(project.name, Semver(nextVer), dryRun, UpToDate.FALSE, false)
}
}

it("should take precedence with commit message keyword over -Pincrement=$inc when -Prelease is NOT set") {
val project = SemverKtTestProject()
// Arrange
Git.open(project.projectDir.toFile()).use {
it.tag().setName("v0.1.0").call() // set initial version
project.projectDir.resolve("text.txt").createFile().writeText("Hello")
it.add().addFilepattern("text.txt").call()
it.commit().setMessage("New commit\n\n[major]")
.call() // set to major to check if lower precedence values will override
it.commit().setMessage("New commit\n\n[patch]")
.call() // set to patch to check if lower precedence value will override the property
}
// Act
val result = fromProperty.tag(project)(r)(dryRun)
val args = if (!dryRun) arrayOf("tag", "-Pincrement=$inc") else arrayOf(
"tag",
"-PdryRun",
"-Pincrement=$inc"
)
val result = Builder.build(project = project, args = args)
// Assert
result.task(":tag")?.outcome shouldBe t.expectedOutcome(dryRun, UpToDate.FALSE)
val nextVer = when (inc) {
"major" -> "1.0.0"
"minor" -> "0.2.0"
"patch" -> "0.1.1"
"pre_release" -> "0.2.0-rc.1"
else -> "42" // shouldn't really get here
}
val nextVer = "0.1.1"
result.output shouldContain t.expectedConfigure(Semver(nextVer), emptyList())
result.output shouldContain t.expectedTag(
project.name,
Expand All @@ -133,15 +197,14 @@ class SemverKtPluginFT : AbstractFT({ t ->
)
}

it("should take precedence with commit message keyword over -Pincrement=$inc when -Prelease is NOT set") {
it("should not create a new tag when [skip] is the last keyword with -Pincrement=$inc when -Prelease is NOT set") {
val project = SemverKtTestProject()
// Arrange
Git.open(project.projectDir.toFile()).use {
it.tag().setName("v0.1.0").call() // set initial version
project.projectDir.resolve("text.txt").createFile().writeText("Hello")
it.add().addFilepattern("text.txt").call()
it.commit().setMessage("New commit\n\n[patch]")
.call() // set to patch to check if lower precedence value will override the property
it.commit().setMessage("[skip] release").call()
}
// Act
val args = if (!dryRun) arrayOf("tag", "-Pincrement=$inc") else arrayOf(
Expand All @@ -151,16 +214,9 @@ class SemverKtPluginFT : AbstractFT({ t ->
)
val result = Builder.build(project = project, args = args)
// Assert
result.task(":tag")?.outcome shouldBe t.expectedOutcome(dryRun, UpToDate.FALSE)
val nextVer = "0.1.1"
result.output shouldContain t.expectedConfigure(Semver(nextVer), emptyList())
result.output shouldContain t.expectedTag(
project.name,
Semver(nextVer),
dryRun,
UpToDate.FALSE,
false
)
result.task(":tag")?.outcome shouldBe t.expectedOutcome(dryRun, UpToDate.TRUE)
result.output shouldContain t.expectedConfigure(Semver("0.0.0"), emptyList())
result.output shouldContain t.expectedTag(project.name, null, dryRun, UpToDate.TRUE, false)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class SemverKtPluginConfigTest : DescribeSpec({
it("should have default configuration intact") {
config.git.tag.separator shouldBe ""
config.git.message.major shouldBe "[major]"
config.git.message.skip shouldBe "[skip]"
config.git.repo.directory shouldBe Path(".")
config.git.repo.cleanRule shouldBe CleanRule.TRACKED
config.version.preReleaseId shouldBe "rc"
Expand Down

0 comments on commit 5799723

Please sign in to comment.