Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 68 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Semantic versioning for Gradle using Git
# Git Semantic Versioning Plugin for Gradle
[![Gradle Build](https://github.com/jmongard/Git.SemVersioning.Gradle/workflows/Gradle%20Build/badge.svg)](https://github.com/jmongard/Git.SemVersioning.Gradle/actions/workflows/gradle-push.yml)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=jmongard_Git.SemVersioning.Gradle&metric=alert_status)](https://sonarcloud.io/dashboard?id=jmongard_Git.SemVersioning.Gradle)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=jmongard_Git.SemVersioning.Gradle&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=jmongard_Git.SemVersioning.Gradle)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=jmongard_Git.SemVersioning.Gradle&metric=coverage)](https://sonarcloud.io/summary/new_code?id=jmongard_Git.SemVersioning.Gradle)
[![GitHub tag (with filter)](https://img.shields.io/github/v/tag/jmongard/Git.SemVersioning.Gradle?logo=gradle&label=Release)](https://plugins.gradle.org/plugin/com.github.jmongard.git-semver-plugin)

Gradle plugin for automatically versioning a project using semantic versioning and conventional commits with change log support based on git commit messages.
A Gradle plugin that automatically versions your project using semantic versioning and conventional commits. It analyzes Git commit messages to determine version increments and generates change logs based on your commit history.


## Usage
Expand Down Expand Up @@ -38,18 +38,13 @@ The plugin requires Gradle 8 and Java version 17 to run.

## Versioning

The versioning system is designed to follow semantic versioning as described by https://semver.org/.
The versioning system follows semantic versioning as described at [semver.org](https://semver.org/).

It works by recursively traversing the commit tree until it finds a version tag or release commit and then calculating
the new version using from there using commit messages.
The plugin works by traversing the Git commit history backwards from HEAD until it finds a version tag or release commit, then calculates the new version based on conventional commit messages since that point.

The plugin will look for [conventional commit](https://www.conventionalcommits.org/) messages (`fix:`, `feat:`, `refactor!:`, ...)
and will increase the corresponding version number.
The plugin recognizes [conventional commit](https://www.conventionalcommits.org/) messages (`fix:`, `feat:`, `refactor!:`, etc.) and increments the corresponding version number accordingly.

The plugin has the opinion that you want to group several fixes/features or breaking changes into a single release.
Therefore, the major, minor or patch number will be increases by at most one compared to the previous release that is
not a pre-release version. Set property `groupVersionIncrements = false` if you don't want the version changes to be combined.
(See [Configuration](#Configuration) reference below.)
By default, the plugin groups multiple fixes/features or breaking changes into a single release. This means the major, minor, or patch number will increase by at most one compared to the previous release (excluding pre-releases). Set `groupVersionIncrements = false` if you prefer each commit to increment the version individually.

### Releases

Expand All @@ -62,12 +57,11 @@ The version number should consist of three numbers separated by a dot e.g. `1.0.
be at the start of the message e.g. `release: v1.2.3` will be matched.


### Uncommited changes or non release commits
### Uncommitted Changes

If no version changed has been triggered by any commit messages since the last release
then the patch number will be increased by one.
If no version increment has been triggered by conventional commit messages since the last release, the patch number will be increased by one to indicate development progress.

If the current version is not a pre-release then `-SNAPSHOT` will be added.
If the current version is not already a pre-release, `-SNAPSHOT` will be appended to indicate this is a development version.


## Version format
Expand Down Expand Up @@ -97,6 +91,46 @@ string will not be semver compliant.
* semver.infoVersion == semver.semVersion.toInfoVersionString("%03d", 0, true, false)
* semver.semVersion.toString() == semver.semVersion.toInfoVersionString("%03d", 7, true, false)

### Two-Digit Versioning

The plugin supports 2-digit versioning (major.minor) in addition to the standard 3-digit semantic versioning (major.minor.patch).
This can be useful for projects that follow a simpler versioning scheme.

To enable 2-digit versioning, set `useTwoDigitVersion = true` in your configuration:

```groovy
semver {
useTwoDigitVersion = true
}
```

#### How 2-Digit Versioning Works

When `useTwoDigitVersion` is enabled:

* **Version Format**: Versions are formatted as `major.minor` (e.g., `5.2` instead of `5.2.0`)
* **Patch Changes**: Fix commits (`fix:`) are treated as minor version changes instead of patch changes
* **Version Bumping**: When no specific version changes are triggered, the minor version is incremented instead of the patch version
* **Pre-releases**: Pre-release versions work the same way (e.g., `5.2-alpha.1`)

#### Example with 2-Digit Versioning

| Command | Commit Text | Calculated version |
| --------------------------------------------- | ------------------------- | ------------------- |
| git commit -m "Initial commit" | Initial commit | 0.1-SNAPSHOT+001 |
| git commit -m "fix: a bug fix" | fix: a bug fix | 0.2-SNAPSHOT+001 |
| gradle releaseVersion | release: v0.2 | 0.2 |
| git commit -m "feat: new feature" | feat: new feature | 0.3-SNAPSHOT+001 |
| git commit -m "feat!: breaking change" | feat!: breaking change | 1.0-SNAPSHOT+002 |
| gradle releaseVersion --preRelease="alpha.1" | release: v1.0-alpha.1 | 1.0-alpha.1 |

#### Accessing 2-Digit Versions

When `useTwoDigitVersion` is enabled, the standard version properties automatically use the 2-digit format:

* `semver.version` - Returns the 2-digit version (e.g., `5.2`)
* `semver.infoVersion` - Returns the 2-digit version with commit count (e.g., `5.2+001`)
* `semver.semVersion.toString()` - Returns the 2-digit version with SHA (e.g., `5.2+001.sha.1c792d5`)

## Tasks

Expand Down Expand Up @@ -124,7 +158,7 @@ $ gradlew printInfoVersion

## `printSemVersion`
This plugin adds a printSemVersion task, which will echo the project's calculated version
to standard-out includning commit count and sha.
to standard-out including commit count and SHA.

````shell
$ gradlew printSemVersion
Expand All @@ -134,13 +168,12 @@ $ gradlew printSemVersion
````

## `printChangeLog`
This plugin adds a printChangeLog task, which will format the commit message for the current version
and output them to standard-out. To avoid enoding problem in the console the change log can be outputed
to an UTF-8 encoded file using `--file <filename>` e.g. `./gradlew printChangeLog --file build/changelog.md`
Note: Use an absolute path for filename as the working directory might not be the one you expect if running
using gradle deamon.
This plugin adds a printChangeLog task, which will format the commit messages for the current version
and output them to standard-out. To avoid encoding problems in the console, the change log can be output
to a UTF-8 encoded file using `--file <filename>`, e.g. `./gradlew printChangeLog --file build/changelog.md`.

Note: The `printChangeLog` task is currently only registered on the root project given that the plugin is applied there.
**Note:** Use an absolute path for the filename as the working directory might not be what you expect when running
using the Gradle daemon. The `printChangeLog` task is currently only registered on the root project when the plugin is applied there.

````shell
$ gradlew printChangeLog
Expand All @@ -158,19 +191,17 @@ $ gradlew printChangeLog
[Configuring the changelog](/ChangeLog.md)

## `releaseVersion`
The `releaseVersion` task will by default create both a release commit, and a release tag. The releaseVersion task will
fail with an error if there exists local modification. It is possible to change this behaviour with the following options:
The `releaseVersion` task creates both a release commit and a release tag by default. The task will fail with an error if there are uncommitted local modifications. You can modify this behavior using the following options:

* **--no-tag**: skip creating a tag. (Can also be set in settings using `createReleaseTag=false`.)
* **--tag**: create a tag (If this has been disabled by the `createReleaseTag=false` option otherwise this is the default.)
* **--no-commit**: skip creating a commit. (Can also be set in settings using `createReleaseCommit=false`.)
* **--commit**: create a commit (If this has been disabled by the `createReleaseCommit=false` option otherwise this is the default.)
* **--no-dirty**: skip dirty check. (Can also be set in settings using `noDirtyCheck=true`.)
* **--message**="a message": Add a message text to the tag and/or commit
* **--preRelease**="pre-release": Change the current pre-release e.g. `--preRelease=alpha.1`.
Set the pre-release to "-" e.g. `--preRelease=-` to promote a pre-release to a release.
* **--no-tag**: Skip creating a tag (can also be set in settings using `createReleaseTag=false`)
* **--tag**: Create a tag (default behavior, unless disabled in settings)
* **--no-commit**: Skip creating a commit (can also be set in settings using `createReleaseCommit=false`)
* **--commit**: Create a commit (default behavior, unless disabled in settings)
* **--no-dirty**: Skip the dirty working directory check (can also be set in settings using `noDirtyCheck=true`)
* **--message**="message": Add a custom message to the tag and/or commit
* **--preRelease**="version": Set the pre-release identifier (e.g., `--preRelease=alpha.1`). Use `--preRelease=-` to promote a pre-release to a full release

Note: The `releaseVersion` task is currently only registered on the root project given that the plugin is applied there.
**Note:** The `releaseVersion` task is currently only registered on the root project when the plugin is applied there.

## Example of how version is calculated
With setting: `groupVersionIncrements = true` (default)
Expand Down Expand Up @@ -260,6 +291,7 @@ semver {
createReleaseCommit = true
createReleaseTag = true
metaSeparator = '+'
useTwoDigitVersion = false
}

//Remember to retrieve the version after plugin has been configured
Expand All @@ -276,7 +308,7 @@ version = semver.version
version tags with "v".
* **groupVersionIncrements**: Used to disable grouping of version increments so that each commit message counts.
* **noDirtyCheck**: Can be used to ignore all local modifications when calculating the version.
Disabling dirty check can also be donne from the command line e.g. `gradlew -PnoDirtyCheck=true someOtherTask`.
Disabling dirty check can also be done from the command line e.g. `gradlew -PnoDirtyCheck=true someOtherTask`.
* **noAutoBump**: If set only commits matching majorPattern, minorPattern or patchPattern will increase the version.
The default behaviour for the plugin is to assume you have begun the work on the next release for any commit you do
after the last release. The patch level (or pre-release level, if the last release was a pre-release) of the version
Expand All @@ -288,6 +320,7 @@ version = semver.version
* **createReleaseCommit**: If a release commit should be created when running the release task. Setting this to false
has the same effect as the --no-commit flag.
* **metaSeparator**: The character to use to separate build metadata from the version when printing info version.
* **useTwoDigitVersion**: If the version should be two digits instead of three.

Patterns is matched using [java regular expressions](https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html)
with IGNORE_CASE and MULTILINE options enabled.
Expand All @@ -296,7 +329,7 @@ with IGNORE_CASE and MULTILINE options enabled.

This plugin has been tested on Gradle 7.x and 8.x. (Version 0.4.3 and older should work on gradle 6.x and probably 5.x)

## Continues Integration
## Continuous Integration
The plugin calculates the version using the commit tree. Make sure you check out all commits relevant and not just
a shallow copy.

Expand Down
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ plugins {
id("jacoco")
}

kotlin {
jvmToolchain(17)
}

semver {
releaseTagNameFormat = "v%s"
createReleaseTag = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ abstract class GitSemverPluginExtension(project: Project, providerFactory: Provi
/**
* The semantic version for the project with commit info excluding sha as a string e.g. "1.2.3-Alpha.4+005"
*/
val infoVersion: String by lazy { semVersion.toInfoVersionString(metaSeparator = metaSeparator) }
val infoVersion: String by lazy { semVersion.toInfoVersionString(
metaSeparator = metaSeparator,
useTwoDigitVersion = useTwoDigitVersion) }

/**
* The semantic version for the project e.g. 1.2.3-Alpha.4
Expand All @@ -115,7 +117,7 @@ abstract class GitSemverPluginExtension(project: Project, providerFactory: Provi
/**
* The semantic version for the project as a string e.g. "1.2.3-Alpha.4"
*/
val version: String by lazy { versionValue.toString() }
val version: String by lazy { versionValue.toString(useTwoDigitVersion) }

private var semInfoVersionValueSource = project.providers.of(SemInfoVersionValueSource::class.java) {
it.parameters.getGitDir().set(gitDirectory);
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/git/semver/plugin/gradle/PrintTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ open class PrintTask @Inject constructor(private val printout: () -> Any, desc:
init {
group = GitSemverPlugin.VERSIONING_GROUP
description = desc
if (!reason.isEmpty()) {
if (reason.isNotEmpty()) {
notCompatibleWithConfigurationCache(reason)
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/main/kotlin/git/semver/plugin/scm/GitProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ internal class GitProvider(private val settings: SemverSettings) {
getHeadCommit(it.repository),
params.preRelease?.trimStart('-')
)
val versionString = version.toInfoVersionString(metaSeparator = settings.metaSeparator)
val versionString = version.toInfoVersionString(
metaSeparator = settings.metaSeparator,
useTwoDigitVersion = settings.useTwoDigitVersion)
logger.info("Saving new version: {}", versionString)

val isCommit = isFormatEnabled(params.commit, settings.releaseCommitTextFormat)
Expand Down
5 changes: 3 additions & 2 deletions src/main/kotlin/git/semver/plugin/semver/BaseSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ abstract class BaseSettings(
var noDirtyCheck: Boolean = false,
var noAutoBump: Boolean = false,
var gitSigning: Boolean? = null, // null means use the jgit default
var metaSeparator: Char = '+'
var metaSeparator: Char = '+',
var useTwoDigitVersion: Boolean = false // Enable 2-digit versioning (major.minor) instead of 3-digit (major.minor.patch)
) : Serializable {
constructor(settings: BaseSettings) : this(
settings.defaultPreRelease, settings.releasePattern, settings.patchPattern, settings.minorPattern,
settings.majorPattern, settings.releaseCommitTextFormat, settings.releaseTagNameFormat,
settings.groupVersionIncrements, settings.noDirtyCheck, settings.noAutoBump,
settings.gitSigning, settings.metaSeparator
settings.gitSigning, settings.metaSeparator, settings.useTwoDigitVersion
)
}
52 changes: 38 additions & 14 deletions src/main/kotlin/git/semver/plugin/semver/MutableSemVersion.kt
Original file line number Diff line number Diff line change
Expand Up @@ -113,19 +113,21 @@ internal class MutableSemVersion(
) {
when {
major > 0 && settings.majorRegex.containsMatchIn(text) ->
if (!isPreRelease || major == initialVersion.major) {
bumpMajor += 1
bumpMinor = 0
bumpPatch = 0
bumpPre = 0
bumpMajor()

settings.useTwoDigitVersion ->
if (settings.minorRegex.containsMatchIn(text) ||
settings.patchRegex.containsMatchIn(text)
) {
if (preRelease.number == null) {
bumpMinor()
} else {
bumpPre += 1
}
}

settings.minorRegex.containsMatchIn(text) ->
if (!isPreRelease || major == initialVersion.major && minor == initialVersion.minor) {
bumpMinor += 1
bumpPatch = 0
bumpPre = 0
}
bumpMinor()

settings.patchRegex.containsMatchIn(text) ->
if (preRelease.number == null) {
Expand All @@ -137,7 +139,24 @@ internal class MutableSemVersion(
}
}

internal fun applyPendingChanges(forceBumpIfNoChanges: Boolean, groupChanges: Boolean): Boolean {
private fun bumpMajor() {
if (!isPreRelease || major == initialVersion.major) {
bumpMajor += 1
bumpMinor = 0
bumpPatch = 0
bumpPre = 0
}
}

private fun bumpMinor() {
if (!isPreRelease || major == initialVersion.major && minor == initialVersion.minor) {
bumpMinor += 1
bumpPatch = 0
bumpPre = 0
}
}

internal fun applyPendingChanges(forceBumpIfNoChanges: Boolean, groupChanges: Boolean, useTwoDigitVersion: Boolean = false): Boolean {
if (hasPendingChanges) {
if (groupChanges) {
applyChangesGrouped()
Expand All @@ -148,17 +167,22 @@ internal class MutableSemVersion(
return true
}

if (!forceBumpIfNoChanges) {
return false
if (forceBumpIfNoChanges) {
forceBump(useTwoDigitVersion)
return true
}
return false
}

private fun forceBump(useTwoDigitVersion: Boolean) {
val preReleaseNumber = preRelease.number
if (preReleaseNumber != null) {
preRelease = preRelease.copy(number = preReleaseNumber + 1)
} else if (useTwoDigitVersion) {
minor += 1
} else {
patch += 1
}
return true
}

private fun applyChangesNotGrouped() {
Expand Down
Loading
Loading