Skip to content

Commit

Permalink
Merge pull request #205 from ajoberstar/fixes
Browse files Browse the repository at this point in the history
minor: Allow commit messages to bump to 1.0.0
  • Loading branch information
ajoberstar authored Nov 24, 2024
2 parents c29b0dc + f681553 commit de2d6b4
Show file tree
Hide file tree
Showing 16 changed files with 166 additions and 51 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ The general form is:
body is not used
```

Where `<scope>` is `major`, `minor`, or `patch` (must be lowercase).
Where `<scope>` is `major`, `minor`, or `patch` (must be lowercase). `major!` is a special value of `<scope>` that can force an upgrade to 1.0.0.

The `(area)` is not used for any programmatic reasons, but could be used by other tools to categorize changes.

Expand Down Expand Up @@ -276,11 +276,13 @@ In this case we'd be looking at all commits since the last tagged final version,

Before 1.0.0, SemVer doesn't really guarantee anything, but a good practice seems to be a `PATCH` increment is for bug fixes, while a `MINOR` increase can be new features or breaking changes.

In order to promote the convention of using `major: My message` for breaking changes, before 1.0.0 a `major` in a commit message will be read as `minor`. The goal is to promote you explicitly documenting breaking changes in your commit logs, while requiring the actual 1.0.0 version bump to come via an override with `-Preckon.scope=major`.
In order to promote the convention of using `major: My message` for breaking changes, before 1.0.0 a `major` in a commit message will be read as `minor`. The goal is to promote you explicitly documenting breaking changes in your commit logs.

The bump to 1.0.0 can happen with either a `major!: My Message` or via an override with `-Preckon.scope=major`.

#### DISCLAIMER this is not Convention Commits compliant

While this approach is similar to [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/), it does not follow their spec, sticking to something more directly applicable to Reckon's scopes. User's can use the `calcScopeFromCommitMessages(Function<String, Optional<Scope>>)` form if they want to implement Conventional Commits, or any other scheme themselves.
While this approach is similar to [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/), it does not follow their spec, sticking to something more directly applicable to Reckon's scopes. User's can use the `calcScopeFromCommitMessageParser(CommitMessageScopeParser)` form if they want to implement Conventional Commits, or any other scheme themselves.

### Tagging and pushing your version

Expand Down
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
32 changes: 16 additions & 16 deletions reckon-core/gradle.lockfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@
# This file is expected to be part of source control.
com.github.zafarkhaja:java-semver:0.10.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.googlecode.javaewah:JavaEWAH:1.2.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
commons-codec:commons-codec:1.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy-agent:1.14.11=testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.14.11=testCompileClasspath,testRuntimeClasspath
org.apache.commons:commons-lang3:3.14.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
commons-codec:commons-codec:1.17.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy-agent:1.15.4=testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.15.4=testCompileClasspath,testRuntimeClasspath
org.apache.commons:commons-lang3:3.17.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath
org.eclipse.jgit:org.eclipse.jgit:6.8.0.202311291450-r=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:5.10.2=testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:5.10.2=testRuntimeClasspath
org.junit.jupiter:junit-jupiter-params:5.10.2=testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter:5.10.2=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-commons:1.10.2=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-engine:1.10.2=testRuntimeClasspath
org.junit.platform:junit-platform-launcher:1.10.2=testRuntimeClasspath
org.junit:junit-bom:5.10.2=testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-core:5.10.0=testCompileClasspath,testRuntimeClasspath
org.eclipse.jgit:org.eclipse.jgit:6.10.0.202406032230-r=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:5.11.3=testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:5.11.3=testRuntimeClasspath
org.junit.jupiter:junit-jupiter-params:5.11.3=testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter:5.11.3=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-commons:1.11.3=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-engine:1.11.3=testRuntimeClasspath
org.junit.platform:junit-platform-launcher:1.11.3=testRuntimeClasspath
org.junit:junit-bom:5.11.3=testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-core:5.14.2=testCompileClasspath,testRuntimeClasspath
org.objenesis:objenesis:3.3=testRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath
org.slf4j:slf4j-api:2.0.12=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.slf4j:slf4j-simple:2.0.12=testRuntimeClasspath
org.slf4j:slf4j-api:2.1.0-alpha1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.slf4j:slf4j-simple:2.1.0-alpha1=testRuntimeClasspath
empty=annotationProcessor,signatures,testAnnotationProcessor
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.ajoberstar.reckon.core;

import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Pattern;

/**
* A functional interface for parsing Git commit messages for Reckon scopes. The implementation can
* decide what convention within the message denotes each scope value.
*/
@FunctionalInterface
public interface CommitMessageScopeParser {
Optional<Scope> parse(String messageBody, boolean preV1);

/**
* Returns a parser that checks the message subject for a prefixed like so:
* {@code <scope>(<area>): subject}. If the project is currently pre-v1, a prefix of {@code major: }
* will be downgraded to {@code minor}, unless you use {@code major!: } with an exclamation point.
*
* @return parser that reads scopes from subject prefixes
*/
static CommitMessageScopeParser subjectPrefix() {
var pattern = Pattern.compile("^(major!|major|minor|patch)(?:\\(.*?\\))?: .+");
return (msg, preV1) -> {
var matcher = pattern.matcher(msg);

if (!matcher.find()) {
return Optional.empty();
}

Scope scope;
switch (matcher.group(1)) {
// the ! forces use of major, ignoring preV1 checks
case "major!":
scope = Scope.MAJOR;
break;
// otherwise we don't allow pre-v1 to bump to major
case "major":
scope = preV1 ? Scope.MINOR : Scope.MAJOR;
break;
case "minor":
scope = Scope.MINOR;
break;
case "patch":
scope = Scope.PATCH;
break;
default:
throw new AssertionError("Unhandled scope value matched by regex: " + matcher.group("scope"));
};
return Optional.of(scope);
};
}

/**
* Adapter for legacy message parsers always prevent bumping to v1.
*
* @param parser legacy parser function
* @return parser that prevents v1 bumps
*/
static CommitMessageScopeParser ofLegacy(Function<String, Optional<Scope>> parser) {
return (messageBody, preV1) -> {
return parser.apply(messageBody).map(scope -> {
if (preV1 && scope == Scope.MAJOR) {
return Scope.MINOR;
} else {
return scope;
}
});
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import java.util.Comparator;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Pattern;

@FunctionalInterface
public interface ScopeCalculator {
Expand All @@ -29,28 +28,36 @@ static ScopeCalculator ofUserString(Function<VcsInventory, Optional<String>> sco
}

/**
* Creates a scope calculator that uses the given function to parse the inventory's commit messages
* Creates a scope calculator that uses the given parser to parse the inventory's commit messages
* for the presence os scope indicators. If any are found, the most significant scope is returned.
*
* @param messageScope function that parses a single commit message for a scope indicator
* @param parser the chosen way to read scopes from commit messages
* @return a legit scope calculator
*/
static ScopeCalculator ofCommitMessage(Function<String, Optional<Scope>> messageScope) {
static ScopeCalculator ofCommitMessageParser(CommitMessageScopeParser parser) {
return inventory -> {
var scope = inventory.getCommitMessages().stream()
.map(messageScope)
.flatMap(Optional::stream)
var preV1 = inventory.getBaseNormal().compareTo(Version.valueOf("1.0.0")) < 0;
return inventory.getCommitMessages().stream()
.flatMap(msg -> parser.parse(msg, preV1).stream())
.max(Comparator.naturalOrder());

// if we're still below 1.0, don't let a commit message push you there
if (Optional.of(Scope.MAJOR).equals(scope) && inventory.getBaseNormal().compareTo(Version.valueOf("1.0.0")) < 0) {
return Optional.of(Scope.MINOR);
} else {
return scope;
}
};
}

/**
* Creates a scope calculator that uses the given function to parse the inventory's commit messages
* for the presence os scope indicators. If any are found, the most significant scope is returned.
* <br/>
* Before v1, MAJOR is always ignored and MINOR is substituted. If that's not desirable, see
* {@link #ofCommitMessageParser(CommitMessageScopeParser)}.
*
* @param messageScope function that parses a single commit message for a scope indicator
* @return a legit scope calculator
*/
static ScopeCalculator ofCommitMessage(Function<String, Optional<Scope>> messageScope) {
var parser = CommitMessageScopeParser.ofLegacy(messageScope);
return ofCommitMessageParser(parser);
}

/**
* Creates a scope calculator that checks commit messages for a prefix of either: "major: ", "minor:
* ", or "patch: " enforcing lower case. Any other commit messages are ignored. Conventionally, you
Expand All @@ -59,14 +66,6 @@ static ScopeCalculator ofCommitMessage(Function<String, Optional<Scope>> message
* @return a legit scope calculator
*/
static ScopeCalculator ofCommitMessages() {
var pattern = Pattern.compile("^(major|minor|patch)(?:\\(.*?\\))?: .+");
return ScopeCalculator.ofCommitMessage(msg -> {
var matcher = pattern.matcher(msg);
if (matcher.find()) {
return Optional.of(Scope.from(matcher.group(1)));
} else {
return Optional.empty();
}
});
return ScopeCalculator.ofCommitMessageParser(CommitMessageScopeParser.subjectPrefix());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@ public void initRepository() throws IOException, GitAPIException {
.setDirectory(repoDir.toFile())
.call();

var config = git.getRepository().getConfig();
config.setString("user", null, "name", "Some Person");
config.setString("user", null, "email", "[email protected]");
config.setString("commit", null, "gpgSign", "false");
config.setString("tag", null, "gpgSign", "false");
config.save();

var initialBranch = git.getRepository().getBranch();

commit();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ public void setupRepo() throws IOException, GitAPIException {
repoDir = Files.createTempDirectory("repo");
git = Git.init().setDirectory(repoDir.toFile()).call();
initialBranch = git.getRepository().getBranch();

var config = git.getRepository().getConfig();
config.setString("user", null, "name", "Some Person");
config.setString("user", null, "email", "[email protected]");
config.setString("commit", null, "gpgSign", "false");
config.setString("tag", null, "gpgSign", "false");
config.save();
}

@AfterEach
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ public void ofCommitMessageNoMatch() {

var inventoryMultiMatchPre1 = getInventoryWithMessages(Version.valueOf("0.7.5"), "some message", "patch: some fix", "major: breaking change");
assertEquals(Optional.of(Scope.MINOR), calc.calculate(inventoryMultiMatchPre1), "Before 1.0 should find the more significant matching scope, but cap at minor");

var inventoryMultiMatchPre1Force = getInventoryWithMessages(Version.valueOf("0.7.5"), "some message", "major!: force to 1.0", "patch: some fix", "major: breaking change");
assertEquals(Optional.of(Scope.MAJOR), calc.calculate(inventoryMultiMatchPre1Force), "Before 1.0, can force 1.0 using major! as a prefix");
}

private VcsInventory getInventoryWithMessages(Version baseNormal, String... messages) {
Expand Down
8 changes: 4 additions & 4 deletions reckon-gradle/gradle.lockfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
# This file is expected to be part of source control.
com.github.zafarkhaja:java-semver:0.10.2=runtimeClasspath,testRuntimeClasspath
com.googlecode.javaewah:JavaEWAH:1.2.3=compatTestCompileClasspath,compatTestRuntimeClasspath,compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
commons-codec:commons-codec:1.16.0=compatTestCompileClasspath,compatTestRuntimeClasspath,compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.commons:commons-lang3:3.14.0=runtimeClasspath,testRuntimeClasspath
commons-codec:commons-codec:1.17.0=compatTestCompileClasspath,compatTestRuntimeClasspath,compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.commons:commons-lang3:3.17.0=runtimeClasspath,testRuntimeClasspath
org.apiguardian:apiguardian-api:1.1.2=compatTestCompileClasspath
org.codehaus.groovy:groovy:3.0.12=compatTestCompileClasspath,compatTestRuntimeClasspath
org.eclipse.jgit:org.eclipse.jgit:6.8.0.202311291450-r=compatTestCompileClasspath,compatTestRuntimeClasspath,compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.jgit:org.eclipse.jgit:6.10.0.202406032230-r=compatTestCompileClasspath,compatTestRuntimeClasspath,compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.hamcrest:hamcrest:2.2=compatTestCompileClasspath,compatTestRuntimeClasspath
org.junit.platform:junit-platform-commons:1.9.0=compatTestCompileClasspath,compatTestRuntimeClasspath
org.junit.platform:junit-platform-engine:1.9.0=compatTestCompileClasspath,compatTestRuntimeClasspath
org.junit:junit-bom:5.9.0=compatTestCompileClasspath,compatTestRuntimeClasspath
org.opentest4j:opentest4j:1.2.0=compatTestCompileClasspath,compatTestRuntimeClasspath
org.slf4j:slf4j-api:1.7.36=compatTestCompileClasspath,compatTestRuntimeClasspath,compileClasspath,testCompileClasspath
org.slf4j:slf4j-api:2.0.12=runtimeClasspath,testRuntimeClasspath
org.slf4j:slf4j-api:2.1.0-alpha1=runtimeClasspath,testRuntimeClasspath
org.spockframework:spock-core:2.3-groovy-3.0=compatTestCompileClasspath,compatTestRuntimeClasspath
empty=annotationProcessor,compatTestAnnotationProcessor,signatures,testAnnotationProcessor
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class BaseCompatTest extends Specification {

def remoteDir = new File(tempDir, 'remote')
remote = Git.init().setDirectory(remoteDir).call()
Gits.resetConfig(remote);

Gits.repoFile(remote, '.gitignore') << '.gradle/\nbuild/\n'
Gits.repoFile(remote, 'master.txt') << 'contents here'
Expand All @@ -31,6 +32,7 @@ class BaseCompatTest extends Specification {

def remote2Dir = new File(tempDir, 'remote2')
remote2 = Gits.clone(remote2Dir, remote)
Gits.resetConfig(remote2);
}

def 'if no git repo found, version is defaulted'() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class CompositeBuildCompatTest extends Specification {
build2File = projectFile(project2Dir, 'build.gradle')

def git1 = Git.init().setDirectory(project1Dir).call()
Gits.resetConfig(git1);
projectFile(project1Dir, 'settings.gradle') << 'rootProject.name = "project1"'
projectFile(project1Dir, '.gitignore') << '.gradle\nbuild\n'
build1File << '''\
Expand All @@ -45,6 +46,7 @@ task printVersion {
git1.close()

def git2 = Git.init().setDirectory(project2Dir).call();
Gits.resetConfig(git2);
projectFile(project2Dir, 'settings.gradle') << 'rootProject.name = "project2"'
projectFile(project2Dir, '.gitignore') << '.gradle\nbuild\n'
build2File << '''\
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
Expand All @@ -14,9 +15,24 @@
public final class Gits {
private static final SecureRandom random = new SecureRandom();

public static void resetConfig(Git git) {
try {
var config = git.getRepository().getConfig();
config.setString("user", null, "name", "Some Person");
config.setString("user", null, "email", "[email protected]");
config.setString("commit", null, "gpgSign", "false");
config.setString("tag", null, "gpgSign", "false");
config.save();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

public static Git clone(File dir, Git origin) throws GitAPIException {
var uri = origin.getRepository().getDirectory().getAbsolutePath();
return Git.cloneRepository().setDirectory(dir).setURI(uri).setCloneAllBranches(true).setNoCheckout(false).call();
var git = Git.cloneRepository().setDirectory(dir).setURI(uri).setCloneAllBranches(true).setNoCheckout(false).call();
resetConfig(git);
return git;
}

public static Path repoFile(Git git, String path) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class SettingsCompatTest extends Specification {

def remoteDir = new File(tempDir, 'remote')
remote = Git.init().setDirectory(remoteDir).call()
Gits.resetConfig(remote);

Gits.repoFile(remote, '.gitignore') << '.gradle/\nbuild/\n'
Gits.repoFile(remote, 'master.txt') << 'contents here'
Expand All @@ -34,6 +35,7 @@ class SettingsCompatTest extends Specification {

def remote2Dir = new File(tempDir, 'remote2')
remote2 = Gits.clone(remote2Dir, remote)
Gits.resetConfig(remote2);
}

def 'if no git repo found, version is defaulted'() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ public ScopeCalculator calcScopeFromCommitMessages(Function<String, Optional<Sco
return ScopeCalculator.ofCommitMessage(messageParser);
}

public ScopeCalculator calcScopeFromCommitMessageParser(CommitMessageScopeParser messageParser) {
return ScopeCalculator.ofCommitMessageParser(messageParser);
}

public StageCalculator calcStageFromProp() {
return StageCalculator.ofUserString((inventory, targetNormal) -> Optional.ofNullable(stage.getOrNull()));
}
Expand Down
6 changes: 3 additions & 3 deletions reckon-gradle/stutter.lockfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# DO NOT MODIFY: Generated by Stutter plugin.
java11=7.0.2,7.6.4,8.0.2,8.9,8.10-rc-1
java17=7.3.3,7.6.4,8.0.2,8.9,8.10-rc-1
java21=8.4,8.9,8.10-rc-1
java11=7.0.2,7.6.4,8.0.2,8.11.1
java17=7.3.3,7.6.4,8.0.2,8.11.1
java21=8.4,8.11.1

0 comments on commit de2d6b4

Please sign in to comment.