From e7f0c06d29562c82cdfe4b651fd45d2200925056 Mon Sep 17 00:00:00 2001 From: papchenko Date: Fri, 16 Oct 2020 20:29:19 +0300 Subject: [PATCH 1/7] feat: produce a meaningful message, if the current directory is not a git repo --- .../semantic/release/common/LogHandler.java | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) 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..e069dee 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,37 @@ 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")); + ObjectId head = repository.resolve(HEAD_COMMIT_ALIAS); + if (head == null) { + return null; + } + RevCommit start = walk.parseCommit(head); 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 +58,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(); } From a86038d46bd6c600217e54841423bf5a9f7ca84b Mon Sep 17 00:00:00 2001 From: papchenko Date: Fri, 16 Oct 2020 20:32:21 +0300 Subject: [PATCH 2/7] feat: produce a meaningful message, if the current directory is not a git repo --- .../smartling/cc4j/semantic/release/common/LogHandler.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 e069dee..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 @@ -37,11 +37,7 @@ 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); - ObjectId head = repository.resolve(HEAD_COMMIT_ALIAS); - if (head == null) { - return null; - } - RevCommit start = walk.parseCommit(head); + RevCommit start = walk.parseCommit(repository.resolve(HEAD_COMMIT_ALIAS)); walk.markStart(start); From ad238437bbd1e029fecff480a861bd1bbdfe5ad8 Mon Sep 17 00:00:00 2001 From: Oleksandr Papchenko Date: Sat, 14 Nov 2020 19:33:00 +0200 Subject: [PATCH 3/7] feat: add changelog file generation feature (#1) feat: add changelog file generation feature --- README.md | 16 ++- .../cc4j/semantic/release/common/Commit.java | 32 +++++ .../release/common/CommitAdapter.java | 2 + .../release/common/GitCommitAdapter.java | 8 +- .../common/changelog/ChangelogExtractor.java | 16 +++ .../common/changelog/ChangelogGenerator.java | 118 ++++++++++++++++++ .../changelog/GitChangelogExtractor.java | 52 ++++++++ .../common/scm/GitRepositoryAdapter.java | 38 ++++++ .../release/common/scm/RepositoryAdapter.java | 6 + .../common/ChangelogGeneratorTest.java | 108 ++++++++++++++++ .../semantic/release/common/CommitTest.java | 16 +++ .../release/common/DummyCommitAdapter.java | 13 ++ .../plugin/maven/AbstractVersioningMojo.java | 37 ++++-- .../maven/ConventionalChangelogMojo.java | 65 ++++++++++ pom.xml | 74 +++++------ 15 files changed, 548 insertions(+), 53 deletions(-) create mode 100644 conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/changelog/ChangelogExtractor.java create mode 100644 conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/changelog/ChangelogGenerator.java create mode 100644 conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/changelog/GitChangelogExtractor.java create mode 100644 conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/scm/GitRepositoryAdapter.java create mode 100644 conventional-commits-common/src/main/java/com/smartling/cc4j/semantic/release/common/scm/RepositoryAdapter.java create mode 100644 conventional-commits-common/src/test/java/com/smartling/cc4j/semantic/release/common/ChangelogGeneratorTest.java create mode 100644 conventional-commits-maven-plugin/src/main/java/com/smartling/cc4j/semantic/plugin/maven/ConventionalChangelogMojo.java diff --git a/README.md b/README.md index 420c3e8..67a23a5 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ -# Conventional Commits for Java +# The fork of Conventional Commits for Java Provides a Java implementation of [Conventional Commits] for projects built with Java 1.8+ using Git for version control. - +The fork include additional goal that enables to generate changelog files automatically ## Maven Plugin ### 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: @@ -27,6 +27,14 @@ In your main `pom.xml` file add the plugin: mvn conventional-commits:version release:prepare mvn release:perform +#### Generate change logs + mvn conventional-commits:version conventional-commits:changelog release:prepare + mvn release:perform +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 to commit 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/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/pom.xml b/pom.xml index de445aa..07b90ba 100644 --- a/pom.xml +++ b/pom.xml @@ -66,44 +66,46 @@ maven-compiler-plugin 3.8.1 - 8 - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.6 - - - --no-tty - --batch - --pinentry-mode - loopback - - - - - sign-artifacts - verify - - sign - - - - - - org.apache.maven.plugins - maven-release-plugin - 2.5.3 - - true - false - true - v@{project.version} - false - ci: + 1.8 + 1.8 + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From fc98a5d41deb017c57fda133c33b943badd53e53 Mon Sep 17 00:00:00 2001 From: Oleksandr Papchenko Date: Sat, 14 Nov 2020 19:38:33 +0200 Subject: [PATCH 4/7] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 67a23a5..3bf4d2a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # The fork of Conventional Commits for Java +**NOTE:** The fork includes additional maven goal which enables to generate changelog files automatically. Provides a Java implementation of [Conventional Commits] for projects built with Java 1.8+ using Git for version control. -The fork include additional goal that enables to generate changelog files automatically ## Maven Plugin ### Usage From f1681dd297b4a47524e7a8bcd2b2eb2a379406fb Mon Sep 17 00:00:00 2001 From: Oleksandr Papchenko Date: Tue, 16 Mar 2021 14:03:57 +0200 Subject: [PATCH 5/7] Changelog (#2) docs: add changelog usage example --- README.md | 45 +++++++++++++++++++++++++++++------ pom.xml | 70 +++++++++++++++++++++++++++---------------------------- 2 files changed, 73 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 3bf4d2a..cad3484 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ -# The fork of Conventional Commits for Java -**NOTE:** The fork includes additional maven goal which enables to generate changelog files automatically. +# Conventional Commits for Java Provides a Java implementation of [Conventional Commits] for projects built with Java 1.8+ using Git for version control. + ## Maven Plugin ### Usage This plugin works together with the [Maven Release Plugin] to create -conventional commit compliant releases for your Maven projects +a conventional commit compliant releases for your Maven projects #### Install the Plugin @@ -22,17 +22,48 @@ 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 -#### Generate change logs +#### With generated change logs + mvn conventional-commits:version conventional-commits:changelog release:prepare mvn release:perform -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 to commit additional files on release:prepare + +#### 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 diff --git a/pom.xml b/pom.xml index 07b90ba..be77b42 100644 --- a/pom.xml +++ b/pom.xml @@ -71,41 +71,41 @@ true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + --no-tty + --batch + --pinentry-mode + loopback + + + + + sign-artifacts + verify + + sign + + + + + + org.apache.maven.plugins + maven-release-plugin + 2.5.3 + + true + false + true + v@{project.version} + false + ci: + + From a38b6a58a520ece6b01096ed8167e4dba76e7637 Mon Sep 17 00:00:00 2001 From: Oleksandr Papchenko Date: Tue, 16 Mar 2021 14:05:00 +0200 Subject: [PATCH 6/7] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cad3484..2589d1d 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,9 @@ ci: add build step ##### Generated change log (CHANGELOG.MD): ## 1.0.0 (2020-11-14) -###Breaking changes +### Breaking changes * change public API [(ticket-23)](http://example.com/ticket-23) [(23b1e004c4)](http://example.com/23b1e004c45b56b633f09656a05875a5a5ff7e86) -###CI +### CI * add build step **Note**: changelog goal performs a commit that includes updated CHANGELOG.MD From 51ca2275156fa0b7c6a94d7a283bb67692415448 Mon Sep 17 00:00:00 2001 From: Oleksandr Papchenko Date: Sun, 21 Mar 2021 18:29:58 +0200 Subject: [PATCH 7/7] Add spot bugs (#3) fix: add spot bugs plugin --- .../maven/ConventionalVersioningMojo.java | 12 ++++++--- pom.xml | 26 ++++++++++++++++++- 2 files changed, 34 insertions(+), 4 deletions(-) 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 be77b42..951c3cc 100644 --- a/pom.xml +++ b/pom.xml @@ -1,4 +1,5 @@ - + 4.0.0 com.smartling.cc4j conventional-commits-parent @@ -106,6 +107,29 @@ ci: + + com.github.spotbugs + spotbugs-maven-plugin + 4.2.0 + + Max + true + + + + + check + + + + + + com.github.spotbugs + spotbugs + 4.2.2 + + +