Skip to content

Commit

Permalink
Merge pull request #172 from ajoberstar/auto
Browse files Browse the repository at this point in the history
Support getting scope from commit messages
  • Loading branch information
ajoberstar authored Feb 12, 2022
2 parents 67a17ea + c9f7f0d commit a0f894b
Show file tree
Hide file tree
Showing 23 changed files with 1,627 additions and 1,281 deletions.
96 changes: 86 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,29 @@ plugins {
}
reckon {
// START As of 0.16.0
// what stages are allowed
stages('milestone', 'rc', 'final')
// or use snapshots
snapshots()
// how do you calculate the scope/stage
scopeCalc = calcScopeFromProp().or(calcScopeFromCommitMessages()) // fall back to commit message (see below) if no explicit prop set
stageCalc = calcScopeFromProp()
// these can also be arbitrary closures (see code for details)
scopeCalc = { inventory -> Optional.of(Scope.MAJOR) }
stageCalc = { inventory, targetNormal -> Optional.of('beta') }
// END As of 0.16.0
// START LEGACY
scopeFromProp()
stageFromProp('milestone', 'rc', 'final')
// alternative to stageFromProp
// snapshotFromProp()
// END LEGACY
// omit this to use the default of 'minor'
defaultInferredScope = 'patch'
Expand All @@ -154,24 +171,83 @@ reckon {

**NOTE:** Reckon overrides the `project.version` property in Gradle

#### Execute Gradle
#### Passing scope/stage as props

Execute Gradle providing the properties, as needed:

- `reckon.scope` - one of `major`, `minor`, or `patch` (defaults to `minor`) to specify which component of the previous release should be incremented
- `reckon.stage`
- (if you used `stageFromProp`) one of the values passed to `stageFromProp` (defaults to the first alphabetically) to specify what phase of development you are in
- (if you used `snapshotFromProp`) either `snapshot` or `final` (defaults to `snapshot`) to specify what phase of development you are in
- `reckon.snapshot` - **deprecated** (if you used `snapshotFromProp`) one of `true` or `false` (defaults to `true`) to determine whether a snapshot should be made
- `reckon.scope` (allowed if `scopeCalc` includes `calcStageFromProp()` or if you called `scopeFromProp()`)
Valid values: `major`, `minor`, `patch` (if not set the scope is inferred by other means)
- `reckon.stage` (allowed if `stageCalc` includes `calcStageFromProp()`or if you used `stageFromProp()` or `snapshotFromProp()`)
- For users of `stages()` or `stageFromProp()`:
Valid values are any stage you listed via those methods. (if not set the stage is inferred by other means)
- For users of `snapshots()` or `snapshotFromProp()`:
Valid values: `snapshot` or `final`

When Gradle executes, the version will be inferred as soon as something tries to access it. This will be output to the console (as below).

```
./gradlew build -Preckon.scope=minor -Preckon.stage=milestone
Reckoned version 1.3.0-milestone.1
...
```

#### Reading scope from commit messages

**NOTE:** This is considered somewhat experimental as of 0.16.0.

If you want the scope to inferred in a more automated way, consider making use of a commit message convention. This sections describes the out-of-the-box convention supported by Reckon. Others are possible by customizing the `scopeCalc` further.

If your `scopeCalc` includes `calcScopeFromCommitMessages()`, the commit messages between your "base normal" (previous final release) and the current `HEAD` are parsed for SemVer indicators.

The general form is:

```
<scope>(optional area of codebase): rest of message
body is not used
```

Where `<scope>` is `major`, `minor`, or `patch` (must be lowercase).

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

Example that would be treated as a `Scope.MAJOR`:

```
major: Dropped support for Gradle 5
This is a breaking change reoving support for Gradle 5 due to use of a new feature in Gradle 6.
```

Example that would be treated as a `Scope.MINOR`:

```
minor(plugin): Dropped support for Gradle 5
This is a breaking change reoving support for Gradle 5 due to use of a new feature in Gradle 6.
```

Take this example commit log:

```
xzy1234 patch: other fixes
abc1234 (tag: 1.2.3) patch: fixed things
def1234 chore(docs): Documenting change to plugin application
ghi1234 (tag: 1.3.0-beta.1) minor: Adding property to override tag message
jkl1234 patch(extension): Fixed support for Provider in extension
mno1234 Other message not following convention
pqr1234 (HEAD -> main) major: Removed deprecated setNormal method
```

In this case we'd be looking at all commits since the last tagged final version, `1.2.3`. We'd only care about messages that follow our convention of prefixing the message with a scope. Since there's a mix of commits using all 3 scopes, we pick the most severe of the ones we found `major`.

##### Special Case for pre-1.0.0

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`.

##### 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.

### Tagging and pushing your version

Reckon's Gradle plugin also provides two tasks:
Expand Down
9 changes: 3 additions & 6 deletions reckon-core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
plugins {
id("org.ajoberstar.defaults.java-library")
groovy
}

group = "org.ajoberstar.reckon"
Expand Down Expand Up @@ -29,18 +28,16 @@ dependencies {
// util
implementation("org.apache.commons:commons-lang3:[3.5,4.0[")
implementation("com.github.zafarkhaja:java-semver:[0.9,)")

// testing

}

testing {
suites {
val test by getting(JvmTestSuite::class) {
useSpock("2.0-groovy-3.0")
useJUnitJupiter("latest.release")
dependencies {
implementation("org.junit.jupiter:junit-jupiter-params")
implementation("org.mockito:mockito-core:latest.release")
implementation("org.ajoberstar.grgit:grgit-core:[5.0,6.0[")
implementation("org.codehaus.groovy:groovy-all:[3.0,4.0[")
runtimeOnly("org.slf4j:slf4j-simple:[1.7.25,1.8.0[")
}
}
Expand Down
63 changes: 12 additions & 51 deletions reckon-core/gradle.lockfile
Original file line number Diff line number Diff line change
@@ -1,65 +1,26 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
cglib:cglib-nodep:3.3.0=testCompileClasspath,testRuntimeClasspath
com.beust:jcommander:1.78=testRuntimeClasspath
com.github.javaparser:javaparser-core:3.23.0=testCompileClasspath,testRuntimeClasspath
com.github.zafarkhaja:java-semver:0.9.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.googlecode.javaewah:JavaEWAH:1.1.13=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.thoughtworks.qdox:qdox:1.12.1=testRuntimeClasspath
info.picocli:picocli:4.6.1=testRuntimeClasspath
jline:jline:2.14.6=testRuntimeClasspath
junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.11.0=testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy-agent:1.12.7=testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.12.7=testCompileClasspath,testRuntimeClasspath
org.ajoberstar.grgit:grgit-core:5.0.0=testCompileClasspath,testRuntimeClasspath
org.apache.ant:ant-antlr:1.10.11=testRuntimeClasspath
org.apache.ant:ant-junit:1.10.11=testRuntimeClasspath
org.apache.ant:ant-launcher:1.10.11=testRuntimeClasspath
org.apache.ant:ant:1.10.11=testCompileClasspath,testRuntimeClasspath
org.apache.commons:commons-lang3:3.12.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apiguardian:apiguardian-api:1.1.0=testCompileClasspath,testRuntimeClasspath
org.assertj:assertj-core:3.16.1=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-all:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-ant:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-astbuilder:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-cli-picocli:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-console:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-datetime:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-docgenerator:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-groovydoc:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-groovysh:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-jmx:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-json:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-jsr223:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-macro:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-nio:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-servlet:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-sql:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-swing:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-templates:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-test-junit5:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-test:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-testng:3.0.9=testCompileClasspath,testRuntimeClasspath
org.codehaus.groovy:groovy-xml:3.0.9=testCompileClasspath,testRuntimeClasspath
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath
org.codehaus.groovy:groovy:3.0.9=testCompileClasspath,testRuntimeClasspath
org.eclipse.jgit:org.eclipse.jgit:6.0.0.202111291000-r=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.hamcrest:hamcrest-core:1.3=testCompileClasspath,testRuntimeClasspath
org.hamcrest:hamcrest:2.2=testCompileClasspath,testRuntimeClasspath
org.jetbrains:annotations:20.1.0=testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:5.7.2=testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:5.7.2=testRuntimeClasspath
org.junit.platform:junit-platform-commons:1.7.2=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-engine:1.7.2=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-launcher:1.7.2=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-testkit:1.7.2=testCompileClasspath,testRuntimeClasspath
org.junit:junit-bom:5.7.2=testCompileClasspath,testRuntimeClasspath
org.objenesis:objenesis:3.2=testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:5.8.2=testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:5.8.2=testRuntimeClasspath
org.junit.jupiter:junit-jupiter-params:5.8.2=testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter:5.8.2=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-commons:1.8.2=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-engine:1.8.2=testRuntimeClasspath
org.junit:junit-bom:5.8.2=testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-core:4.3.1=testCompileClasspath,testRuntimeClasspath
org.objenesis:objenesis:3.2=testRuntimeClasspath
org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm:9.1=testCompileClasspath,testRuntimeClasspath
org.slf4j:slf4j-api:1.7.30=compileClasspath,runtimeClasspath,testCompileClasspath
org.slf4j:slf4j-api:1.7.36=testRuntimeClasspath
org.slf4j:slf4j-simple:1.7.36=testRuntimeClasspath
org.spockframework:spock-core:2.0-groovy-3.0=testCompileClasspath,testRuntimeClasspath
org.testng:testng:7.4.0=testRuntimeClasspath
org.webjars:jquery:3.5.1=testRuntimeClasspath
empty=
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
Expand Down Expand Up @@ -48,42 +49,44 @@ public GitInventorySupplier(Repository repo, VersionTagParser tagParser) {
@Override
public VcsInventory getInventory() {
// share this walk throughout to benefit from its caching
try (ObjectReader reader = repo.newObjectReader(); RevWalk walk = new RevWalk(reader)) {
try (var reader = repo.newObjectReader(); RevWalk walk = new RevWalk(reader)) {
// saves on some performance as we don't really need the commit bodys
walk.setRetainBody(false);

ObjectId headObjectId = repo.getRefDatabase().findRef("HEAD").getObjectId();
var headObjectId = repo.getRefDatabase().findRef("HEAD").getObjectId();

if (headObjectId == null) {
logger.debug("No HEAD commit. Presuming repo is empty.");
return new VcsInventory(null, isClean(), null, null, null, 0, null, null);
return new VcsInventory(null, isClean(), null, null, null, 0, null, null, null);
}

logger.debug("Found HEAD commit {}", headObjectId);

RevCommit headCommit = walk.parseCommit(headObjectId);
var headCommit = walk.parseCommit(headObjectId);

Set<TaggedVersion> taggedVersions = getTaggedVersions(walk);
var taggedVersions = getTaggedVersions(walk);

logger.debug("Found tagged versions: {}", taggedVersions);

Version currentVersion = findCurrent(headCommit, taggedVersions.stream())
var currentVersion = findCurrent(headCommit, taggedVersions.stream())
.map(TaggedVersion::getVersion)
.orElse(null);
TaggedVersion baseNormal = findBase(walk, headCommit, taggedVersions.stream().filter(TaggedVersion::isNormal));
TaggedVersion baseVersion = findBase(walk, headCommit, taggedVersions.stream());
var baseNormal = findBase(walk, headCommit, taggedVersions.stream().filter(TaggedVersion::isNormal));
var baseVersion = findBase(walk, headCommit, taggedVersions.stream());

int commitsSinceBase = RevWalkUtils.count(walk, headCommit, baseNormal.getCommit());
var commitsSinceBase = RevWalkUtils.count(walk, headCommit, baseNormal.getCommit());

Set<TaggedVersion> parallelCandidates = findParallelCandidates(walk, headCommit, taggedVersions);
var parallelCandidates = findParallelCandidates(walk, headCommit, taggedVersions);

Set<RevCommit> taggedCommits = taggedVersions.stream().map(TaggedVersion::getCommit).collect(Collectors.toSet());
Set<Version> parallelVersions = parallelCandidates.stream()
var taggedCommits = taggedVersions.stream().map(TaggedVersion::getCommit).collect(Collectors.toSet());
var parallelVersions = parallelCandidates.stream()
.map(version -> findParallel(walk, headCommit, version, taggedCommits))
.flatMap(Optional::stream)
.collect(Collectors.toSet());

Set<Version> claimedVersions = taggedVersions.stream().map(TaggedVersion::getVersion).collect(Collectors.toSet());
var claimedVersions = taggedVersions.stream().map(TaggedVersion::getVersion).collect(Collectors.toSet());

var commitMessages = findCommitMessages(walk, headCommit, baseNormal.getCommit());

return new VcsInventory(
reader.abbreviate(headObjectId).name(),
Expand All @@ -93,7 +96,8 @@ public VcsInventory getInventory() {
baseNormal.getVersion(),
commitsSinceBase,
parallelVersions,
claimedVersions);
claimedVersions,
commitMessages);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
Expand All @@ -110,16 +114,16 @@ private boolean isClean() {
}

private Set<TaggedVersion> getTaggedVersions(RevWalk walk) throws IOException {
Set<TaggedVersion> versions = new HashSet<>();
var versions = new HashSet<TaggedVersion>();

for (Ref ref : repo.getRefDatabase().getRefsByPrefix(Constants.R_TAGS)) {
for (var ref : repo.getRefDatabase().getRefsByPrefix(Constants.R_TAGS)) {

Ref tag = repo.getRefDatabase().peel(ref);
var tag = repo.getRefDatabase().peel(ref);
// only annotated tags return a peeled object id
ObjectId objectId = tag.getPeeledObjectId() == null ? tag.getObjectId() : tag.getPeeledObjectId();
RevCommit commit = walk.parseCommit(objectId);
var objectId = tag.getPeeledObjectId() == null ? tag.getObjectId() : tag.getPeeledObjectId();
var commit = walk.parseCommit(objectId);

String tagName = Repository.shortenRefName(ref.getName());
var tagName = Repository.shortenRefName(ref.getName());
tagParser.parse(tagName).ifPresent(version -> {
versions.add(new TaggedVersion(version, commit));
});
Expand All @@ -139,15 +143,15 @@ private TaggedVersion findBase(RevWalk walk, RevCommit head, Stream<TaggedVersio
walk.setRevFilter(RevFilter.ALL);
walk.markStart(head);

Map<RevCommit, List<TaggedVersion>> versionsByCommit = versions.collect(Collectors.groupingBy(TaggedVersion::getCommit));
var versionsByCommit = versions.collect(Collectors.groupingBy(TaggedVersion::getCommit));

Stream.Builder<List<TaggedVersion>> builder = Stream.builder();

for (RevCommit commit : walk) {
List<TaggedVersion> matches = versionsByCommit.get(commit);
for (var commit : walk) {
var matches = versionsByCommit.get(commit);
if (matches != null) {
// Parents can't be "nearer". Exclude them to avoid extra walking.
for (RevCommit parent : commit.getParents()) {
for (var parent : commit.getParents()) {
walk.markUninteresting(parent);
}
builder.accept(matches);
Expand Down Expand Up @@ -187,11 +191,11 @@ private Optional<Version> findParallel(RevWalk walk, RevCommit head, TaggedVersi
walk.markStart(head);
walk.markStart(candidate.getCommit());

RevCommit mergeBase = walk.next();
var mergeBase = walk.next();

walk.reset();
walk.setRevFilter(RevFilter.ALL);
boolean taggedSinceMergeBase = RevWalkUtils.find(walk, head, mergeBase).stream()
var taggedSinceMergeBase = RevWalkUtils.find(walk, head, mergeBase).stream()
.anyMatch(tagged::contains);

if (mergeBase != null
Expand All @@ -207,6 +211,21 @@ private Optional<Version> findParallel(RevWalk walk, RevCommit head, TaggedVersi
}
}

private List<String> findCommitMessages(RevWalk walk, RevCommit head, RevCommit base) {
try {
walk.reset();
walk.setRevFilter(RevFilter.ALL);
var messages = new ArrayList<String>();
for (var commit : RevWalkUtils.find(walk, head, base)) {
walk.parseBody(commit);
messages.add(commit.getFullMessage());
}
return messages;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

private static class TaggedVersion {
private final Version version;
private final RevCommit commit;
Expand Down
Loading

0 comments on commit a0f894b

Please sign in to comment.