Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

minor: Allow commit messages to bump to 1.0.0 #205

Merged
merged 5 commits into from
Nov 24, 2024
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
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", "some.person@example.com");
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", "some.person@example.com");
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", "some.person@example.com");
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
Loading