diff --git a/README.md b/README.md
index 420c3e8..2589d1d 100644
--- a/README.md
+++ b/README.md
@@ -7,11 +7,11 @@ with Java 1.8+ using Git for version control.
### Usage
-This plugin works together with the [Maven Release Plugin] to create
-conventional commit compliant releases for your Maven projects
+This plugin works together with the [Maven Release Plugin] to create
+a conventional commit compliant releases for your Maven projects
#### Install the Plugin
-
+
In your main `pom.xml` file add the plugin:
@@ -22,11 +22,50 @@ In your main `pom.xml` file add the plugin:
+You can provide the link to you tracking system as parameter in configuration. In generated change log there will be
+ the link to the ticket.
+
+ http://example.com/%s
+
+`%s` - will be replaced by ticket id provided at the begging of message in square brackets.
+For example:
+
+`fix: [ticket-id] message`
+
+Also, you can provide the pattern for repository URL. In the generated change log
+there will be a commit hash with URL to the commit in the remote repository.
+
+ http://example.com/%s
+
#### Release a Version
mvn conventional-commits:version release:prepare
mvn release:perform
+#### With generated change logs
+
+ mvn conventional-commits:version conventional-commits:changelog release:prepare
+ mvn release:perform
+
+#### Changelog example
+
+##### Commit messages:
+breaking change: [ticket-23] change public API
+
+ci: add build step
+
+##### Generated change log (CHANGELOG.MD):
+## 1.0.0 (2020-11-14)
+### Breaking changes
+* change public API [(ticket-23)](http://example.com/ticket-23) [(23b1e004c4)](http://example.com/23b1e004c45b56b633f09656a05875a5a5ff7e86)
+### CI
+* add build step
+
+**Note**: changelog goal performs a commit that includes updated CHANGELOG.MD
+this commit will not be rolled back on release:clean - this is because of well-known
+maven limitation - release plugin does not allow committing additional files on release:prepare
+stage
+
## Gradle Plugin
A [Gradle] plugin is planned for future release.
diff --git a/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/Commit.java b/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/Commit.java
index 427d2bb..41725a1 100644
--- a/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/Commit.java
+++ b/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/Commit.java
@@ -2,11 +2,13 @@
import java.util.Objects;
import java.util.Optional;
+import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Commit
{
private static final Pattern BREAKING_REGEX = Pattern.compile("^(fix|feat)!.+", Pattern.CASE_INSENSITIVE);
+ private static final String TRACKING_SYSTEM_REGEX_STRING = "^\\s*\\[\\s*(.*)\\s*\\]\\s*";
private final CommitAdapter commit;
@@ -67,6 +69,36 @@ public Optional getCommitType()
return Optional.ofNullable(type);
}
+ public Optional getCommitMessageDescription() {
+ return getCommitMessageFullDescription()
+ .map(fullCommitMessage -> fullCommitMessage.replaceFirst(TRACKING_SYSTEM_REGEX_STRING, ""));
+ }
+
+ public Optional getTrackingSystemId() {
+ return getCommitMessageFullDescription().map(commitMessage -> {
+ if("".equals(commitMessage.trim())) {
+ return null;
+ }
+
+ Matcher matcher = Pattern.compile(TRACKING_SYSTEM_REGEX_STRING + ".*", Pattern.CASE_INSENSITIVE).matcher(commitMessage);
+ return matcher.matches() ? matcher.group(1).trim() : null;
+ });
+ }
+
+ public String getCommitHash() {
+ return this.commit.getCommitHash();
+ }
+
+ private Optional getCommitMessageFullDescription() {
+ String message = getMessageForComparison();
+ String[] split = message.split(":");
+ if(split.length <= 1) {
+ return Optional.empty();
+ }
+
+ return Optional.of(split[1].trim());
+ }
+
private String getMessageForComparison()
{
String msg = commit.getShortMessage();
diff --git a/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/CommitAdapter.java b/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/CommitAdapter.java
index e6add67..985f65a 100644
--- a/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/CommitAdapter.java
+++ b/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/CommitAdapter.java
@@ -5,4 +5,6 @@ public interface CommitAdapter
String getShortMessage();
T getCommit();
+
+ String getCommitHash();
}
diff --git a/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/GitCommitAdapter.java b/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/GitCommitAdapter.java
index 3a5edec..0ddf734 100644
--- a/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/GitCommitAdapter.java
+++ b/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/GitCommitAdapter.java
@@ -8,7 +8,7 @@ public class GitCommitAdapter implements CommitAdapter
{
private final RevCommit commit;
- GitCommitAdapter(RevCommit commit)
+ public GitCommitAdapter(RevCommit commit)
{
Objects.requireNonNull(commit, "commit cannot be null");
this.commit = commit;
@@ -25,4 +25,10 @@ public RevCommit getCommit()
{
return commit;
}
+
+ @Override
+ public String getCommitHash()
+ {
+ return commit.getName();
+ }
}
diff --git a/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/LogHandler.java b/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/LogHandler.java
index 3dcee96..7929f28 100644
--- a/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/LogHandler.java
+++ b/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/LogHandler.java
@@ -5,8 +5,10 @@
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryCache;
import org.eclipse.jgit.revplot.PlotWalk;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.util.FS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -15,34 +17,33 @@
import java.util.Objects;
import java.util.stream.Collectors;
-public class LogHandler
-{
+public class LogHandler {
+ public static final String HEAD_COMMIT_ALIAS = "HEAD";
private final Logger logger = LoggerFactory.getLogger(LogHandler.class);
private final Repository repository;
private final Git git;
- public LogHandler(Repository repository)
- {
+ public LogHandler(Repository repository) {
Objects.requireNonNull(repository, "repository cannot be null");
+ if (!RepositoryCache.FileKey.isGitRepository(repository.getDirectory(), FS.DETECTED)) {
+ throw new IllegalArgumentException("Current working directory is not a git repository or " + HEAD_COMMIT_ALIAS + " is missing");
+ }
this.repository = repository;
this.git = Git.wrap(repository);
}
- RevCommit getLastTaggedCommit() throws IOException, GitAPIException
- {
+ RevCommit getLastTaggedCommit() throws IOException, GitAPIException {
List[ tags = git.tagList().call();
List peeledTags = tags.stream().map(t -> repository.peel(t).getPeeledObjectId()).collect(Collectors.toList());
PlotWalk walk = new PlotWalk(repository);
- RevCommit start = walk.parseCommit(repository.resolve("HEAD"));
+ RevCommit start = walk.parseCommit(repository.resolve(HEAD_COMMIT_ALIAS));
walk.markStart(start);
RevCommit revCommit;
- while ((revCommit = walk.next()) != null)
- {
- if (peeledTags.contains(revCommit.getId()))
- {
+ while ((revCommit = walk.next()) != null) {
+ if (peeledTags.contains(revCommit.getId())) {
logger.debug("Found commit matching last tag: {}", revCommit);
break;
}
@@ -53,13 +54,11 @@ RevCommit getLastTaggedCommit() throws IOException, GitAPIException
return revCommit;
}
- public Iterable getCommitsSinceLastTag() throws IOException, GitAPIException
- {
- ObjectId start = repository.resolve("HEAD");
+ public Iterable getCommitsSinceLastTag() throws IOException, GitAPIException {
+ ObjectId start = repository.resolve(HEAD_COMMIT_ALIAS);
RevCommit lastCommit = this.getLastTaggedCommit();
- if (lastCommit == null)
- {
+ if (lastCommit == null) {
logger.warn("No annotated tags found matching any commits on branch");
return git.log().call();
}
diff --git a/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/changelog/ChangelogExtractor.java b/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/changelog/ChangelogExtractor.java
new file mode 100644
index 0000000..a35246d
--- /dev/null
+++ b/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/changelog/ChangelogExtractor.java
@@ -0,0 +1,16 @@
+package com.smartling.cc4j.semantic.release.common.changelog;
+
+import com.smartling.cc4j.semantic.release.common.Commit;
+import com.smartling.cc4j.semantic.release.common.ConventionalCommitType;
+import com.smartling.cc4j.semantic.release.common.scm.ScmApiException;
+
+import java.util.Map;
+import java.util.Set;
+
+public interface ChangelogExtractor {
+ /**
+ * Extracts and groups commits by their commit types
+ * @return - commits grouped by commit type
+ */
+ Map> getGroupedCommitsByCommitTypes() throws ScmApiException;
+}
diff --git a/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/changelog/ChangelogGenerator.java b/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/changelog/ChangelogGenerator.java
new file mode 100644
index 0000000..18959a8
--- /dev/null
+++ b/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/changelog/ChangelogGenerator.java
@@ -0,0 +1,118 @@
+package com.smartling.cc4j.semantic.release.common.changelog;
+
+import com.smartling.cc4j.semantic.release.common.Commit;
+import com.smartling.cc4j.semantic.release.common.ConventionalCommitType;
+import com.smartling.cc4j.semantic.release.common.LogHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.LocalDate;
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class ChangelogGenerator {
+ public static final int COMMIT_HASH_DISPLAYED_LENGTH = 10;
+ private final Logger logger = LoggerFactory.getLogger(LogHandler.class);
+
+ private static final String CHANGELOG_FORMAT = "## %s (%s)" +
+ "%s";
+ private static final String MD_LINK_FORMAT = "[%s](%s)";
+ private static final String BUG_FIXES_HEADER = "Bug fixes";
+ private static final String FEATURE_HEADER = "Feature";
+ private static final String BREAKING_HEADER = "Breaking changes";
+ private static final String DOCS_HEADER = "Docs";
+ private static final String CI_HEADER = "CI";
+ private static final String BUILD_HEADER = "Build";
+
+ private final String repoUrlFormat;
+ private final String trackingSystemUrlFormat;
+
+ public ChangelogGenerator(String repoUrlFormat, String trackingSystemUrlFormat) {
+ this.repoUrlFormat = repoUrlFormat;
+ this.trackingSystemUrlFormat = trackingSystemUrlFormat;
+ }
+
+ public String generate(String nextVersion, Map> commitsByCommitType) {
+ Objects.requireNonNull(nextVersion, "next version can not be null");
+ Objects.requireNonNull(commitsByCommitType, "commits by type can not be null");
+
+ List sections = new ArrayList<>();
+
+ if (commitsByCommitType.get(ConventionalCommitType.BREAKING_CHANGE) != null) {
+ getSection(BREAKING_HEADER, commitsByCommitType.get(ConventionalCommitType.BREAKING_CHANGE)).ifPresent(sections::add);
+ }
+
+ if (commitsByCommitType.get(ConventionalCommitType.FIX) != null) {
+ getSection(BUG_FIXES_HEADER, commitsByCommitType.get(ConventionalCommitType.FIX)).ifPresent(sections::add);
+ }
+
+ if (commitsByCommitType.get(ConventionalCommitType.FEAT) != null) {
+ getSection(FEATURE_HEADER, commitsByCommitType.get(ConventionalCommitType.FEAT)).ifPresent(sections::add);
+ }
+
+ if (commitsByCommitType.get(ConventionalCommitType.DOCS) != null) {
+ getSection(DOCS_HEADER, commitsByCommitType.get(ConventionalCommitType.DOCS)).ifPresent(sections::add);
+ }
+
+ if (commitsByCommitType.get(ConventionalCommitType.CI) != null) {
+ getSection(CI_HEADER, commitsByCommitType.get(ConventionalCommitType.CI)).ifPresent(sections::add);
+ }
+
+ if (commitsByCommitType.get(ConventionalCommitType.BUILD) != null) {
+ getSection(BUILD_HEADER, commitsByCommitType.get(ConventionalCommitType.BUILD)).ifPresent(sections::add);
+ }
+
+ sections = sections.stream().filter(Objects::nonNull).collect(Collectors.toList());
+
+ return String.format(CHANGELOG_FORMAT, nextVersion, LocalDate.now(), "\n" + String.join("\n", sections));
+ }
+
+ private Optional getSection(String header, Set commits) {
+ String sectionEntries = getSectionEntries(commits);
+ if (sectionEntries != null && !sectionEntries.trim().equals("")) {
+ return Optional.of("###" + header + "\n" + sectionEntries);
+ }
+
+ return Optional.empty();
+ }
+
+ private String getSectionEntries(Set commits) {
+ Set uniqueMessages = new HashSet<>();
+ return commits.stream()
+ .filter(commit -> commit.getCommitMessageDescription().isPresent() && uniqueMessages.add(commit.getCommitMessageDescription().get()))
+ .map(this::getChangeLogEntry)
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .sorted()
+ .collect(Collectors.joining("\n"));
+ }
+
+ private Optional getChangeLogEntry(Commit commit) {
+ if (!commit.getCommitMessageDescription().isPresent()) {
+ logger.warn("Skipping message for commit: {}", commit.getCommitHash());
+ }
+ return commit.getCommitMessageDescription().map(message -> {
+ if (commit.getCommitMessageDescription().get().trim().equals("")) {
+ logger.warn("Skipping message for commit: {}", commit.getCommitHash());
+ return null;
+ }
+ return "* " + commit.getCommitMessageDescription().get() + getTrackingSystemLink(commit) + getCommitHashLink(commit);
+ });
+ }
+
+ private String getCommitHashLink(Commit commit) {
+ if (this.repoUrlFormat == null) {
+ return " (" + commit.getCommitHash().substring(0, COMMIT_HASH_DISPLAYED_LENGTH) + ")";
+ } else {
+ return " " + String.format(MD_LINK_FORMAT, "(" + commit.getCommitHash().substring(0, COMMIT_HASH_DISPLAYED_LENGTH) + ")", String.format(this.repoUrlFormat, commit.getCommitHash()));
+ }
+ }
+
+ private String getTrackingSystemLink(Commit commit) {
+ if (this.trackingSystemUrlFormat == null || !commit.getTrackingSystemId().isPresent()) {
+ return commit.getTrackingSystemId().map(s -> " (" + s + ")").orElse("");
+ } else {
+ return " " + String.format(MD_LINK_FORMAT, "(" + commit.getTrackingSystemId().get() + ")", String.format(this.trackingSystemUrlFormat, commit.getTrackingSystemId().get()));
+ }
+ }
+}
diff --git a/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/changelog/GitChangelogExtractor.java b/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/changelog/GitChangelogExtractor.java
new file mode 100644
index 0000000..c825436
--- /dev/null
+++ b/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/changelog/GitChangelogExtractor.java
@@ -0,0 +1,52 @@
+package com.smartling.cc4j.semantic.release.common.changelog;
+
+import com.smartling.cc4j.semantic.release.common.*;
+import com.smartling.cc4j.semantic.release.common.scm.ScmApiException;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import java.io.IOException;
+import java.util.*;
+
+public class GitChangelogExtractor implements ChangelogExtractor {
+ private ConventionalVersioning conventionalVersioning;
+
+ public GitChangelogExtractor(ConventionalVersioning conventionalVersioning) {
+ this.conventionalVersioning = conventionalVersioning;
+ }
+
+ @Override
+ public Map> getGroupedCommitsByCommitTypes() throws ScmApiException {
+
+ LogHandler logHandler = conventionalVersioning.logHandler();
+
+ try {
+ Iterable commits = logHandler.getCommitsSinceLastTag();
+ Map> res = new HashMap<>();
+
+ if (commits == null) {
+ return res;
+ }
+
+ List commitList = new ArrayList<>();
+ commits.iterator().forEachRemaining(c -> commitList.add(new Commit(new GitCommitAdapter(c))));
+
+ for (Commit c : commitList) {
+ if (c.isConventional() && c.getCommitType().isPresent()) {
+ Optional commitType = c.getCommitType();
+ res.compute(commitType.get(), (type, commitsForType) -> {
+ if (commitsForType == null) {
+ return new HashSet<>(Collections.singletonList(c));
+ }
+ commitsForType.add(c);
+ return commitsForType;
+ });
+ }
+ }
+
+ return res;
+ } catch (GitAPIException | IOException e) {
+ throw new ScmApiException("Git operation failed", e);
+ }
+ }
+}
diff --git a/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/scm/GitRepositoryAdapter.java b/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/scm/GitRepositoryAdapter.java
new file mode 100644
index 0000000..4134fc8
--- /dev/null
+++ b/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/scm/GitRepositoryAdapter.java
@@ -0,0 +1,38 @@
+package com.smartling.cc4j.semantic.release.common.scm;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.Repository;
+
+import java.util.Objects;
+
+public class GitRepositoryAdapter implements RepositoryAdapter {
+
+ private final Repository repository;
+ private final Git git;
+
+ public GitRepositoryAdapter(Repository repository) {
+ this.repository = repository;
+ this.git = Git.wrap(repository);
+ }
+
+ @Override
+ public void addFile(String pattern) throws ScmApiException {
+ try {
+ this.git.add().addFilepattern(pattern).call();
+ } catch (GitAPIException e) {
+ throw new ScmApiException("Failed to add file: " + pattern, e);
+ }
+ }
+
+ @Override
+ public void commit(String message) throws ScmApiException {
+ Objects.requireNonNull(message, "commit message can not be null");
+
+ try {
+ this.git.commit().setMessage(message).call();
+ } catch (GitAPIException e) {
+ throw new ScmApiException("Failed to perform commit", e);
+ }
+ }
+}
diff --git a/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/scm/RepositoryAdapter.java b/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/scm/RepositoryAdapter.java
new file mode 100644
index 0000000..8fc02cd
--- /dev/null
+++ b/conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/scm/RepositoryAdapter.java
@@ -0,0 +1,6 @@
+package com.smartling.cc4j.semantic.release.common.scm;
+
+public interface RepositoryAdapter {
+ void addFile(String pattern) throws ScmApiException;
+ void commit(String message) throws ScmApiException;
+}
diff --git a/conventional-commits-common/src/test/java/com/smartling/cc4j/semantic/release/common/ChangelogGeneratorTest.java b/conventional-commits-common/src/test/java/com/smartling/cc4j/semantic/release/common/ChangelogGeneratorTest.java
new file mode 100644
index 0000000..7e4fdff
--- /dev/null
+++ b/conventional-commits-common/src/test/java/com/smartling/cc4j/semantic/release/common/ChangelogGeneratorTest.java
@@ -0,0 +1,108 @@
+package com.smartling.cc4j.semantic.release.common;
+
+import com.smartling.cc4j.semantic.release.common.changelog.ChangelogGenerator;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.time.LocalDate;
+import java.util.*;
+
+import static org.junit.Assert.assertEquals;
+
+public class ChangelogGeneratorTest {
+ private static final String COMMIT_HASH = "2717635691";
+ private static final String TRACKING_SYSTEM_URL_FORMAT = "http://test.com?id=%s";
+ private static final String REPO_URL_FORMAT = "http://repo.com?id=%s";
+
+ private ChangelogGenerator changelogGenerator;
+
+ private static final String EXPECTED_CHANGELOG =
+ "###Breaking changes\n" +
+ "* breaking change test [(2717635691)](http://repo.com?id=2717635691)\n" +
+ "###Bug fixes\n" +
+ "* fix test 2 [(2717635691)](http://repo.com?id=2717635691)\n" +
+ "* fix test [(2717635691)](http://repo.com?id=2717635691)\n" +
+ "* fix ui test [(ticket-id)](http://test.com?id=ticket-id) [(2717635691)](http://repo.com?id=2717635691)\n" +
+ "###Feature\n" +
+ "* fix test [(2717635691)](http://repo.com?id=2717635691)\n" +
+ "###Docs\n" +
+ "* docs test [(2717635691)](http://repo.com?id=2717635691)\n" +
+ "###CI\n" +
+ "* ci test [(2717635691)](http://repo.com?id=2717635691)\n" +
+ "###Build\n" +
+ "* build test [(2717635691)](http://repo.com?id=2717635691)";
+
+ private static final String EXPECTED_CHANGELOG_NO_URLS = "###Breaking changes\n" +
+ "* breaking change test (2717635691)\n" +
+ "###Bug fixes\n" +
+ "* fix test (2717635691)\n" +
+ "* fix test 2 (2717635691)\n" +
+ "* fix ui test (ticket-id) (2717635691)\n" +
+ "###Feature\n" +
+ "* fix test (2717635691)\n" +
+ "###Docs\n" +
+ "* docs test (2717635691)\n" +
+ "###CI\n" +
+ "* ci test (2717635691)\n" +
+ "###Build\n" +
+ "* build test (2717635691)";
+
+ @Before
+ public void setUp() {
+ changelogGenerator = new ChangelogGenerator(REPO_URL_FORMAT, TRACKING_SYSTEM_URL_FORMAT);
+ }
+
+ @Test
+ public void testOnlyHeaderGeneratedOnEmptyChanges() {
+ Map> commits = new HashMap<>();
+ assertEquals(getExpectedChangelogHeader("0.0.1"), changelogGenerator.generate("0.0.1", commits));
+
+ commits.put(ConventionalCommitType.FEAT, new HashSet<>(Collections.singletonList(
+ new Commit(
+ new DummyCommitAdapter("ci this message will not be included to changelog as there is no colon", COMMIT_HASH)))));
+ assertEquals(getExpectedChangelogHeader("0.0.1"), changelogGenerator.generate("0.0.1", commits));
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void testVersionIsMandatory() {
+ changelogGenerator.generate(null, Collections.emptyMap());
+ }
+
+ @Test
+ public void testChangelogGenerated() {
+ Map> commitsByCommitType = getCommitsByCommitType();
+ String changelog = changelogGenerator.generate("0.2.7", commitsByCommitType);
+ assertEquals(EXPECTED_CHANGELOG, changelog.substring(changelog.indexOf("\n") + 1));
+ }
+
+ @Test
+ public void testChangelogGeneratedNoUrlFormatsProvided() {
+ Map> commitsByCommitType = getCommitsByCommitType();
+ String changelog = new ChangelogGenerator(null, null).generate("0.2.7", commitsByCommitType);
+ assertEquals(EXPECTED_CHANGELOG_NO_URLS, changelog.substring(changelog.indexOf("\n") + 1));
+ }
+
+ private Map> getCommitsByCommitType() {
+ Map> res = new HashMap<>();
+ res.put(ConventionalCommitType.BREAKING_CHANGE,
+ new HashSet<>(Collections.singletonList(new Commit(new DummyCommitAdapter("breaking change: breaking change test", COMMIT_HASH)))));
+ res.put(ConventionalCommitType.FEAT,
+ new HashSet<>(Collections.singletonList(new Commit(new DummyCommitAdapter("feat(ui): fix test", COMMIT_HASH)))));
+ res.put(ConventionalCommitType.FIX,
+ new HashSet<>(Arrays.asList(new Commit(new DummyCommitAdapter("fix(ui): [TICKET-ID] fix ui test", COMMIT_HASH)),
+ new Commit(new DummyCommitAdapter("fix(ui): fix test", COMMIT_HASH)),
+ new Commit(new DummyCommitAdapter("fix(ui): fix test 2", COMMIT_HASH)))));
+ res.put(ConventionalCommitType.CI,
+ new HashSet<>(Arrays.asList(new Commit(new DummyCommitAdapter("ci: ci test", COMMIT_HASH)),
+ new Commit(new DummyCommitAdapter("ci this message will not me included to changelog as there is no colon", COMMIT_HASH)))));
+ res.put(ConventionalCommitType.BUILD,
+ new HashSet<>(Collections.singletonList(new Commit(new DummyCommitAdapter("build: build test", COMMIT_HASH)))));
+ res.put(ConventionalCommitType.DOCS,
+ new HashSet<>(Collections.singletonList(new Commit(new DummyCommitAdapter("docs: docs test", COMMIT_HASH)))));
+ return res;
+ }
+
+ private String getExpectedChangelogHeader(String version) {
+ return "## " + version + " (" + LocalDate.now() + ")\n";
+ }
+}
diff --git a/conventional-commits-common/src/test/java/com/smartling/cc4j/semantic/release/common/CommitTest.java b/conventional-commits-common/src/test/java/com/smartling/cc4j/semantic/release/common/CommitTest.java
index 1186dce..4119449 100644
--- a/conventional-commits-common/src/test/java/com/smartling/cc4j/semantic/release/common/CommitTest.java
+++ b/conventional-commits-common/src/test/java/com/smartling/cc4j/semantic/release/common/CommitTest.java
@@ -48,6 +48,22 @@ public void getCommitTypeFix()
assertEquals(ConventionalCommitType.FIX, create("fix(scope): foo").getCommitType().get());
}
+ @Test
+ public void getMessage() {
+ assertEquals("commit message", create("fix: commit message").getCommitMessageDescription().get());
+ assertEquals("commit message", create("fix: [22] commit message").getCommitMessageDescription().get());
+ assertFalse( create("fix commit message").getCommitMessageDescription().isPresent());
+ }
+
+ @Test
+ public void getTrackingSystemId() {
+ assertEquals("22", create("fix: [22] commit message").getTrackingSystemId().get());
+ assertEquals("22", create("fix: [22 ] commit message").getTrackingSystemId().get());
+ assertEquals("22", create("fix:[ 22 ] commit message").getTrackingSystemId().get());
+ assertFalse(create("fix [22] commit message").getTrackingSystemId().isPresent());
+ assertFalse(create("fix: commit message").getTrackingSystemId().isPresent());
+ }
+
static Commit create(String shortMessage)
{
return new Commit(new DummyCommitAdapter(shortMessage));
diff --git a/conventional-commits-common/src/test/java/com/smartling/cc4j/semantic/release/common/DummyCommitAdapter.java b/conventional-commits-common/src/test/java/com/smartling/cc4j/semantic/release/common/DummyCommitAdapter.java
index 19339b4..510b40c 100644
--- a/conventional-commits-common/src/test/java/com/smartling/cc4j/semantic/release/common/DummyCommitAdapter.java
+++ b/conventional-commits-common/src/test/java/com/smartling/cc4j/semantic/release/common/DummyCommitAdapter.java
@@ -3,10 +3,18 @@
class DummyCommitAdapter implements CommitAdapter
{
private final String shortMessage;
+ private final String hash;
DummyCommitAdapter(String shortMessage)
{
this.shortMessage = shortMessage;
+ this.hash = null;
+ }
+
+ DummyCommitAdapter(String shortMessage, String hash)
+ {
+ this.shortMessage = shortMessage;
+ this.hash = hash;
}
@Override
@@ -20,4 +28,9 @@ public DummyCommitAdapter getCommit()
{
return null;
}
+
+ @Override
+ public String getCommitHash() {
+ return this.hash;
+ }
}
diff --git a/conventional-commits-maven-plugin/src/main/java/com/smartling/cc4j/semantic/plugin/maven/AbstractVersioningMojo.java b/conventional-commits-maven-plugin/src/main/java/com/smartling/cc4j/semantic/plugin/maven/AbstractVersioningMojo.java
index 084c3fc..cb6c90f 100644
--- a/conventional-commits-maven-plugin/src/main/java/com/smartling/cc4j/semantic/plugin/maven/AbstractVersioningMojo.java
+++ b/conventional-commits-maven-plugin/src/main/java/com/smartling/cc4j/semantic/plugin/maven/AbstractVersioningMojo.java
@@ -4,6 +4,10 @@
import com.smartling.cc4j.semantic.release.common.ConventionalVersioning;
import com.smartling.cc4j.semantic.release.common.SemanticVersion;
import com.smartling.cc4j.semantic.release.common.SemanticVersionChange;
+import com.smartling.cc4j.semantic.release.common.changelog.ChangelogExtractor;
+import com.smartling.cc4j.semantic.release.common.changelog.GitChangelogExtractor;
+import com.smartling.cc4j.semantic.release.common.scm.GitRepositoryAdapter;
+import com.smartling.cc4j.semantic.release.common.scm.RepositoryAdapter;
import com.smartling.cc4j.semantic.release.common.scm.ScmApiException;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugins.annotations.Parameter;
@@ -14,16 +18,14 @@
import java.io.File;
import java.io.IOException;
import java.util.List;
-import java.util.Objects;
import java.util.Properties;
-abstract class AbstractVersioningMojo extends AbstractMojo
-{
+abstract class AbstractVersioningMojo extends AbstractMojo {
private final static String MVN_RELEASE_VERSION_PROPERTY = "releaseVersion";
private final static String MVN_DEVELOPMENT_VERSION_PROPERTY = "developmentVersion";
@Parameter(defaultValue = "${project.basedir}", required = true)
- private File baseDir;
+ protected File baseDir;
@Parameter(defaultValue = "${project.build.directory}", property = "outputDir", required = true)
File outputDirectory;
@@ -34,28 +36,33 @@ abstract class AbstractVersioningMojo extends AbstractMojo
@Parameter(defaultValue = "${reactorProjects}", readonly = true, required = true)
private List reactorProjects;
- ConventionalVersioning getConventionalVersioning() throws IOException
- {
+ ConventionalVersioning getConventionalVersioning() throws IOException {
Repository repository = new RepositoryBuilder().setWorkTree(baseDir).build();
//Repository repository = new RepositoryBuilder().findGitDir().build();
MavenConventionalVersioning mvnConventionalVersioning = new MavenConventionalVersioning(repository);
return mvnConventionalVersioning.getConventionalVersioning();
}
- Properties createReleaseProperties() throws IOException, ScmApiException
- {
- ConventionalVersioning versioning = this.getConventionalVersioning();
+ RepositoryAdapter getRepositoryAdapter() throws IOException {
+ Repository repository = new RepositoryBuilder().setWorkTree(baseDir).build();
+ return new GitRepositoryAdapter(repository);
+ }
+
+ ChangelogExtractor getChangelogExtractor() throws IOException {
+ return new GitChangelogExtractor(this.getConventionalVersioning());
+ }
+
+ Properties createReleaseProperties() throws IOException, ScmApiException {
Properties props = new Properties();
- SemanticVersion nextVersion = versioning.getNextVersion(SemanticVersion.parse(versionString.replace("-SNAPSHOT", "")));
+ SemanticVersion nextVersion = getNextVersion();
SemanticVersion nextDevelopmentVersion = nextVersion.nextVersion(SemanticVersionChange.PATCH);
// set properties for release plugin
props.setProperty(MVN_RELEASE_VERSION_PROPERTY, nextVersion.toString());
props.setProperty(MVN_DEVELOPMENT_VERSION_PROPERTY, nextDevelopmentVersion.toString() + "-SNAPSHOT");
- for (MavenProject project : reactorProjects)
- {
+ for (MavenProject project : reactorProjects) {
String projectKey = project.getGroupId() + ":" + project.getArtifactId();
props.setProperty("project.rel." + projectKey, nextVersion.toString());
props.setProperty("project.dev." + projectKey, nextDevelopmentVersion.toString());
@@ -63,4 +70,10 @@ Properties createReleaseProperties() throws IOException, ScmApiException
return props;
}
+
+ SemanticVersion getNextVersion() throws IOException, ScmApiException {
+ return this.getConventionalVersioning()
+ .getNextVersion(SemanticVersion
+ .parse(versionString.replace("-SNAPSHOT", "")));
+ }
}
diff --git a/conventional-commits-maven-plugin/src/main/java/com/smartling/cc4j/semantic/plugin/maven/ConventionalChangelogMojo.java b/conventional-commits-maven-plugin/src/main/java/com/smartling/cc4j/semantic/plugin/maven/ConventionalChangelogMojo.java
new file mode 100644
index 0000000..1ef80bc
--- /dev/null
+++ b/conventional-commits-maven-plugin/src/main/java/com/smartling/cc4j/semantic/plugin/maven/ConventionalChangelogMojo.java
@@ -0,0 +1,65 @@
+package com.smartling.cc4j.semantic.plugin.maven;
+
+import com.smartling.cc4j.semantic.release.common.Commit;
+import com.smartling.cc4j.semantic.release.common.ConventionalCommitType;
+import com.smartling.cc4j.semantic.release.common.changelog.ChangelogGenerator;
+import com.smartling.cc4j.semantic.release.common.scm.RepositoryAdapter;
+import com.smartling.cc4j.semantic.release.common.scm.ScmApiException;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+
+@Mojo(name = "changelog", aggregator = true, defaultPhase = LifecyclePhase.VALIDATE)
+public class ConventionalChangelogMojo extends AbstractVersioningMojo {
+
+ private static final String CHANGELOG_FILE_NAME = "CHANGELOG.MD";
+
+ @Parameter( property = "conventional-commits-maven-plugin.repoUrlFormat")
+ private String repoUrlFormat;
+
+ @Parameter( property = "conventional-commits-maven-plugin.trackingSystemUrlFormat")
+ private String trackingSystemUrlFormat;
+
+ @Override
+ public void execute() throws MojoExecutionException {
+ try {
+ Map> commitsByCommitTypes = this
+ .getChangelogExtractor()
+ .getGroupedCommitsByCommitTypes();
+
+ ChangelogGenerator changelogGenerator = new ChangelogGenerator(repoUrlFormat, trackingSystemUrlFormat);
+ String changeLogs = changelogGenerator.generate(this.getNextVersion().toString(), commitsByCommitTypes);
+ appendChangeLogs(changeLogs);
+ commitChanges();
+ } catch (IOException | ScmApiException e) {
+ throw new MojoExecutionException("SCM error: " + e.getMessage(), e);
+ }
+ }
+
+ private void appendChangeLogs(String changeLogs) throws IOException {
+ Path changelogPath = Paths.get(this.baseDir.getAbsolutePath(), CHANGELOG_FILE_NAME);
+ if(!Files.exists(changelogPath)) {
+ Files.createFile(changelogPath);
+ }
+
+ List resultChangeLogs = new ArrayList<>();
+ resultChangeLogs.add(changeLogs);
+ List prevChangeLogs = Files.readAllLines(changelogPath);
+ resultChangeLogs.addAll(prevChangeLogs);
+ Files.write(changelogPath, resultChangeLogs);
+ }
+
+ private void commitChanges() throws IOException, ScmApiException {
+ RepositoryAdapter repositoryAdapter = getRepositoryAdapter();
+ repositoryAdapter.addFile(CHANGELOG_FILE_NAME);
+ repositoryAdapter.commit("ci: update changelog");
+ }
+}
diff --git a/conventional-commits-maven-plugin/src/main/java/com/smartling/cc4j/semantic/plugin/maven/ConventionalVersioningMojo.java b/conventional-commits-maven-plugin/src/main/java/com/smartling/cc4j/semantic/plugin/maven/ConventionalVersioningMojo.java
index 3779416..a28e2e7 100644
--- a/conventional-commits-maven-plugin/src/main/java/com/smartling/cc4j/semantic/plugin/maven/ConventionalVersioningMojo.java
+++ b/conventional-commits-maven-plugin/src/main/java/com/smartling/cc4j/semantic/plugin/maven/ConventionalVersioningMojo.java
@@ -11,12 +11,15 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
+import java.nio.file.Files;
import java.util.Locale;
import java.util.Properties;
@Mojo(name = "version", aggregator = true, defaultPhase = LifecyclePhase.VALIDATE)
public class ConventionalVersioningMojo extends AbstractVersioningMojo
{
+ private static final String VERSION_FILE_NAME = "version.props";
+
@Parameter(defaultValue = "${session}", readonly = true, required = true)
private MavenSession session;
@@ -40,12 +43,15 @@ private void writeVersionFile(Properties props) throws MojoExecutionException
{
File f = outputDirectory;
- if (!f.exists())
+ try
+ {
+ Files.createDirectories(f.toPath());
+ } catch (IOException e)
{
- f.mkdirs();
+ throw new MojoExecutionException("Failed to create output dir: " + f.getAbsolutePath(), e);
}
- File touch = new File(f, "version.props");
+ File touch = new File(f, VERSION_FILE_NAME);
try (OutputStream out = new FileOutputStream(touch))
{
diff --git a/pom.xml b/pom.xml
index de445aa..951c3cc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,4 +1,5 @@
-
+
4.0.0
com.smartling.cc4j
conventional-commits-parent
@@ -66,7 +67,9 @@
maven-compiler-plugin
3.8.1
- 8
+ 1.8
+ 1.8
+ true
@@ -104,6 +107,29 @@
ci:
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+ 4.2.0
+
+ Max
+ true
+
+
+
+
+ check
+
+
+
+
+
+ com.github.spotbugs
+ spotbugs
+ 4.2.2
+
+
+
]