Skip to content

Commit

Permalink
minor: Allow commit messages to bump to 1.0.0
Browse files Browse the repository at this point in the history
As reported in #204, I had unfortunately forced the preference to not
allow commit messages to bump to v1 on all users of commit message
scope calculators. In theory, I had meant to allow custom parsing, but
the custom parsing still enforced the pre-1.0 special casing.

The new approach implements a new ofCommitMessageParser() mechanism
that has the parsing function accept both the commit message and a
boolean indicating whether the project is pre-v1.0.0. This allows the
user full control of the approach they want to use.

To simplify adoption of this new capability the default commit message
parser was enhanced to allow "major!: subject" prefixes that bypass
the pre-1.0.0 check, allowing a more CD style bump to 1.0.0.

Fixes #204
  • Loading branch information
ajoberstar committed Nov 24, 2024
1 parent c29b0dc commit d88de13
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 26 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
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 @@ -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
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

0 comments on commit d88de13

Please sign in to comment.