Skip to content
Open
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
43 changes: 41 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,60 @@ with Java 1.8+ using Git for version control.

### Usage

This plugin works together with the [Maven Release Plugin] to create
This plugin works together with the [Maven Release Plugin] to create
conventional commit compliant releases for your Maven projects

#### Install the Plugin

In your main `pom.xml` file add the plugin:

```xml
<plugins>
<plugin>
<groupId>com.smartling.cc4j</groupId>
<artifactId>conventional-commits-maven-plugin</artifactId>
<version>${version}</version>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<scmCommentPrefix>build(release): </scmCommentPrefix>
<scmDevelopmentCommitComment>ci(release): open next snapshot version</scmDevelopmentCommitComment>
<scmReleaseCommitComment>build(release): publish</scmReleaseCommitComment>
</configuration>
</plugin>
</plugins>

```

In your CI code:

- Execute only for master|main branch
- skip builds for build(release) and ci(release) commits

GitHub workflow:

```yaml
jobs:
main:
...
if: |
"!contains(github.event.head_commit.message, 'build(release)')" &&
"!contains(github.event.head_commit.message, 'ci(release)')"
```

Gitlab:

```yaml
only:
variables:
- '$CI_COMMIT_MESSAGE !~ /^(build|ci)\(release\):.+/'
- '$CI_COMMIT_BRANCH =~ /^(master|main)/'
```

#### Release a Version

mvn conventional-commits:version release:prepare
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@

public class Commit
{
private static final Pattern BREAKING_REGEX = Pattern.compile("^(fix|feat)!.+", Pattern.CASE_INSENSITIVE);
private static final Pattern BREAKING_REGEX = Pattern.compile(
"^((build|chore|ci|docs|fix|feat|refactor|style|test)[a-z0-9\\(\\)]*)((\\!([\\s]*:(.|\\n)*))|" +
"([\\s]*:(.|\\n)*(BREAKING(\\s|-)CHANGE)(.|\\n)*))", Pattern.CASE_INSENSITIVE);

private static final Pattern CONVENTIONAL_COMMIT_REGEX = Pattern.compile(
"^((build|chore|ci|docs|fix|feat|refactor|style|test)[a-z0-9\\(\\)]*)((\\!([\\s]*:" +
"(.|\\n)*))|([\\s]*:(.|\\n)*))", Pattern.CASE_INSENSITIVE);
private final CommitAdapter commit;

public Commit(CommitAdapter commit)
Expand Down Expand Up @@ -40,30 +45,23 @@ public Optional<ConventionalCommitType> getCommitType()
return Optional.of(ConventionalCommitType.BREAKING_CHANGE);
}

for (ConventionalCommitType cc : ConventionalCommitType.values())
if (CONVENTIONAL_COMMIT_REGEX.matcher(msg).matches())
{
for (String t : cc.getCommitTypes())
{
if (msg.startsWith(t))
for (ConventionalCommitType cc : ConventionalCommitType.values()) {
if (ConventionalCommitType.BREAKING_CHANGE.equals(cc))
{
type = cc;
break;
continue;
}
}
}

// FIXME: check for breaking change in footer
/*
for (FooterLine footerLine : commit.getFooterLines())
{
if (footerLine.getKey().equalsIgnoreCase(ConventionalCommitType.BREAKING_CHANGE.getCommitTypes().get(0))) {
type = ConventionalCommitType.BREAKING_CHANGE;
break;
for (String t : cc.getCommitTypes()) {
if (msg.startsWith(t)) {
type = cc;
break;
}
}
}
}

*/

return Optional.ofNullable(type);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,21 @@

public enum ConventionalCommitType implements Comparable<ConventionalCommitType>
{
BREAKING_CHANGE(SemanticVersionChange.MAJOR, "breaking change", "!"),
BREAKING_CHANGE(SemanticVersionChange.MAJOR, "", "!"),
BUILD(SemanticVersionChange.NONE, "build"),
CHORE(SemanticVersionChange.MINOR, "chore"),
CI(SemanticVersionChange.NONE, "ci"),
DOCS(SemanticVersionChange.PATCH, "docs"),
FIX(SemanticVersionChange.PATCH, "fix"),
FEAT(SemanticVersionChange.MINOR, "feat"),
REFACTOR(SemanticVersionChange.MINOR, "refactor"),
STYLE(SemanticVersionChange.NONE, "style"),
TEST(SemanticVersionChange.NONE, "test");

private final List<String> commitTypes;
private final SemanticVersionChange changeType;

ConventionalCommitType(SemanticVersionChange change, String... commitTypes)
ConventionalCommitType(final SemanticVersionChange change, final String... commitTypes)
{
this.commitTypes = Arrays.asList(commitTypes);
this.changeType = change;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class GitCommitAdapter implements CommitAdapter<RevCommit>
{
private final RevCommit commit;

GitCommitAdapter(RevCommit commit)
GitCommitAdapter(final RevCommit commit)
{
Objects.requireNonNull(commit, "commit cannot be null");
this.commit = commit;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ public SemanticVersionChange getNextVersionChangeType() throws ScmApiException,
{
try
{
Iterable<RevCommit> commits = logHandler().getCommitsSinceLastTag();
SemanticVersionChangeResolver resolver = semanticVersionChangeResolver();
final Iterable<RevCommit> commits = logHandler().getCommitsSinceLastTag();
final SemanticVersionChangeResolver resolver = semanticVersionChangeResolver();

return resolver.resolveChange(commits);
}
catch (GitAPIException e)
catch (final GitAPIException e)
{
throw new ScmApiException("Git operation failed", e);
}
Expand All @@ -48,7 +48,7 @@ public SemanticVersionChange getNextVersionChangeType() throws ScmApiException,
@Override
public SemanticVersion getNextVersion(SemanticVersion currentVersion) throws IOException, ScmApiException
{
SemanticVersionChange change = this.getNextVersionChangeType();
final SemanticVersionChange change = this.getNextVersionChangeType();
return currentVersion.nextVersion(change);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class LogHandler
private final Repository repository;
private final Git git;

public LogHandler(Repository repository)
public LogHandler(final Repository repository)
{
Objects.requireNonNull(repository, "repository cannot be null");
this.repository = repository;
Expand All @@ -31,10 +31,10 @@ public LogHandler(Repository repository)

RevCommit getLastTaggedCommit() throws IOException, GitAPIException
{
List<Ref> tags = git.tagList().call();
List<ObjectId> peeledTags = tags.stream().map(t -> repository.peel(t).getPeeledObjectId()).collect(Collectors.toList());
PlotWalk walk = new PlotWalk(repository);
RevCommit start = walk.parseCommit(repository.resolve("HEAD"));
final List<Ref> tags = git.tagList().call();
final List<ObjectId> peeledTags = tags.stream().map(t -> repository.peel(t).getPeeledObjectId()).collect(Collectors.toList());
final PlotWalk walk = new PlotWalk(repository);
final RevCommit start = walk.parseCommit(repository.resolve("HEAD"));

walk.markStart(start);

Expand All @@ -55,8 +55,8 @@ RevCommit getLastTaggedCommit() throws IOException, GitAPIException

public Iterable<RevCommit> getCommitsSinceLastTag() throws IOException, GitAPIException
{
ObjectId start = repository.resolve("HEAD");
RevCommit lastCommit = this.getLastTaggedCommit();
final ObjectId start = repository.resolve("HEAD");
final RevCommit lastCommit = this.getLastTaggedCommit();

if (lastCommit == null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@

public final class SemanticVersion
{
public static final String VERSION_SUFFIX_SNAPSHOT = "-SNAPSHOT";

private final Integer major;
private final Integer minor;
private final Integer patch;

public SemanticVersion(int major, int minor, int patch)
public SemanticVersion(final int major, final int minor, final int patch)
{
if (major < 0 || minor < 0 || patch < 0)
throw new IllegalArgumentException("versions can only contain positive integers");
Expand All @@ -20,7 +22,7 @@ public SemanticVersion(int major, int minor, int patch)
}

@Override
public boolean equals(Object o)
public boolean equals(final Object o)
{
if (this == o)
{
Expand Down Expand Up @@ -55,9 +57,9 @@ public int getPatch()
return patch;
}

public SemanticVersion nextVersion(SemanticVersionChange change)
public SemanticVersion nextVersion(final SemanticVersionChange change)
{
SemanticVersion nextVersion;
final SemanticVersion nextVersion;

switch (change)
{
Expand All @@ -68,8 +70,6 @@ public SemanticVersion nextVersion(SemanticVersionChange change)
nextVersion = new SemanticVersion(major, minor + 1, 0);
break;
case PATCH:
nextVersion = new SemanticVersion(major, minor, patch + 1);
break;
case NONE:
nextVersion = new SemanticVersion(major, minor, patch);
break;
Expand All @@ -85,10 +85,10 @@ public String toString()
return toString(major, minor, patch);
}

public static SemanticVersion parse(String version)
public static SemanticVersion parse(final String version)
{
Objects.requireNonNull(version, "version required");
String[] parts = version.split("\\.");
final String[] parts = version.replace(VERSION_SUFFIX_SNAPSHOT, "").split("\\.");

if (parts.length != 3)
throw new IllegalArgumentException("Invalid semantic version: " + version);
Expand All @@ -100,8 +100,13 @@ public static SemanticVersion parse(String version)
);
}

private static String toString(int major, int minor, int patch)
private static String toString(final int major, final int minor, final int patch)
{
return String.format(Locale.US, "%d.%d.%d", major, minor, patch);
}

public String getDevelopmentVersionString()
{
return this.toString() + VERSION_SUFFIX_SNAPSHOT;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@
public class SimpleSemanticVersionChangeResolver implements SemanticVersionChangeResolver
{
@Override
public SemanticVersionChange resolveChange(Iterable<RevCommit> commits)
public SemanticVersionChange resolveChange(final Iterable<RevCommit> commits)
{
Objects.requireNonNull(commits, "commits may not be null");
List<Commit> commitList = new ArrayList<>();
final List<Commit> commitList = new ArrayList<>();
commits.iterator().forEachRemaining(c -> commitList.add(new Commit(new GitCommitAdapter(c))));
SemanticVersionChange change = SemanticVersionChange.NONE;

for (Commit c : commitList)
for (final Commit c : commitList)
{
Optional<ConventionalCommitType> commitType = c.getCommitType();
final Optional<ConventionalCommitType> commitType = c.getCommitType();
if (commitType.isPresent())
{
if (SemanticVersionChange.MAJOR.equals(commitType.get().getChangeType()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

public class ScmApiException extends Exception
{
public ScmApiException(String message, Throwable cause)
public ScmApiException(final String message, final Throwable cause)
{
super(message, cause);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import org.junit.Test;

import java.util.Optional;

import static org.junit.Assert.*;

public class CommitTest
Expand All @@ -20,36 +22,60 @@ public void isConventional()
@Test
public void getCommitTypeBreakingChange()
{
assertEquals(ConventionalCommitType.BREAKING_CHANGE, create("breaking change: new version").getCommitType().get());
assertEquals(ConventionalCommitType.BREAKING_CHANGE, create("Breaking Change: add foo").getCommitType().get());
assertEquals(ConventionalCommitType.BREAKING_CHANGE, create("BREAKING CHANGE(scope): add foo").getCommitType().get());
assertFalse(create("breaking change: new version").getCommitType().isPresent());
assertFalse(create("Breaking Change: add foo").getCommitType().isPresent());
assertFalse(create("BREAKING CHANGE(scope): add foo").getCommitType().isPresent());
}

@Test
public void getCommitTypeBreakingChangeExclamation()
{
assertEquals(ConventionalCommitType.BREAKING_CHANGE, create("fix!: new version").getCommitType().get());
assertEquals(ConventionalCommitType.BREAKING_CHANGE, create("feat!: new version").getCommitType().get());
assertEquals(ConventionalCommitType.TEST, create("test!: new version").getCommitType().get());
final Optional<ConventionalCommitType> ctFix = create("fix!: new version").getCommitType();
assertTrue(ctFix.isPresent());
assertEquals(ConventionalCommitType.BREAKING_CHANGE, ctFix.get());

final Optional<ConventionalCommitType> ctFeatBreaking = create("feat!: new version").getCommitType();
assertTrue(ctFeatBreaking.isPresent());
assertEquals(ConventionalCommitType.BREAKING_CHANGE, ctFeatBreaking.get());

final Optional<ConventionalCommitType> ctTestBreaking = create("test!: new version").getCommitType();
assertTrue(ctTestBreaking.isPresent());
assertEquals(ConventionalCommitType.BREAKING_CHANGE, ctTestBreaking.get());
}

@Test
public void getCommitTypeFeat()
{
assertEquals(ConventionalCommitType.FEAT, create("feat: add foo").getCommitType().get());
assertEquals(ConventionalCommitType.FEAT, create("Feat: add foo").getCommitType().get());
assertEquals(ConventionalCommitType.FEAT, create("feat(scope): add foo").getCommitType().get());
final Optional<ConventionalCommitType> ctFeat = create("feat: add foo").getCommitType();
assertTrue(ctFeat.isPresent());
assertEquals(ConventionalCommitType.FEAT, ctFeat.get());

final Optional<ConventionalCommitType> ctFeat2 = create("Feat: add foo").getCommitType();
assertTrue(ctFeat2.isPresent());
assertEquals(ConventionalCommitType.FEAT, ctFeat2.get());

final Optional<ConventionalCommitType> ctFeat3 = create("feat(scope): add foo").getCommitType();
assertTrue(ctFeat3.isPresent());
assertEquals(ConventionalCommitType.FEAT, ctFeat3.get());
}

@Test
public void getCommitTypeFix()
{
assertEquals(ConventionalCommitType.FIX, create("fix: foo").getCommitType().get());
assertEquals(ConventionalCommitType.FIX, create("Fix: foo").getCommitType().get());
assertEquals(ConventionalCommitType.FIX, create("fix(scope): foo").getCommitType().get());
final Optional<ConventionalCommitType> ctFix = create("fix: foo").getCommitType();
assertTrue(ctFix.isPresent());
assertEquals(ConventionalCommitType.FIX, ctFix.get());

final Optional<ConventionalCommitType> ctFix2 = create("Fix: foo").getCommitType();
assertTrue(ctFix2.isPresent());
assertEquals(ConventionalCommitType.FIX, ctFix2.get());

final Optional<ConventionalCommitType> ctFix3 = create("fix(scope): foo").getCommitType();
assertTrue(ctFix3.isPresent());
assertEquals(ConventionalCommitType.FIX, ctFix3.get());
}

static Commit create(String shortMessage)
static Commit create(final String shortMessage)
{
return new Commit(new DummyCommitAdapter(shortMessage));
}
Expand Down
Loading