diff --git a/src/functionalTest/kotlin/git/semver/plugin/gradle/GitSemverPluginFunctionalTest.kt b/src/functionalTest/kotlin/git/semver/plugin/gradle/GitSemverPluginFunctionalTest.kt index 5d23af0..ac45a65 100644 --- a/src/functionalTest/kotlin/git/semver/plugin/gradle/GitSemverPluginFunctionalTest.kt +++ b/src/functionalTest/kotlin/git/semver/plugin/gradle/GitSemverPluginFunctionalTest.kt @@ -119,33 +119,11 @@ class GitSemverPluginFunctionalTest { createReleaseCommit = true // changeLogFormat = ChangeLogFormat.scopeChangeLog changeLogFormat { - appendLine("# My changelog").appendLine() - - withBreakingChanges { - appendLine("## Breaking changes") - format { - "- ${'$'}{fullHeader()}" - } - appendLine() - } - withType("fix") { - appendLine("## Bug Fixes") - format { - "- ${'$'}{scope()}${'$'}{header()}" - } - appendLine() - } - withType("feat") { - appendLine("## Features") - format { - "- ${'$'}{scope()}${'$'}{header()}" - } - appendLine() - } + appendLine("# Test changelog").appendLine() withType("test") { appendLine("## Test") - format { - "- ${'$'}{scope()}${'$'}{header()}" + formatChanges { + appendLine("- ${'$'}{scope()}${'$'}{header()}") } appendLine() } diff --git a/src/main/kotlin/git/semver/plugin/changelog/ChangeLogBuilder.kt b/src/main/kotlin/git/semver/plugin/changelog/ChangeLogBuilder.kt new file mode 100644 index 0000000..377fcba --- /dev/null +++ b/src/main/kotlin/git/semver/plugin/changelog/ChangeLogBuilder.kt @@ -0,0 +1,127 @@ +package git.semver.plugin.changelog + +import java.util.* + +open class ChangeLogBuilder( + val groupKey: String? = null, + private val commitInfos: List, + private val context: ChangeLogFormat.Context +) : DocumentBuilder() { + + fun formatChanges(block: ChangeLogTextFormatter.() -> Unit) { + for (commitInfo in commitInfos()) { + val formatter = ChangeLogTextFormatter(commitInfo) + formatter.block() + appendLine(formatter.build()) + context.flagCommit(commitInfo) + } + } + + fun skip() { + for (commitInfo in commitInfos()) { + context.flagCommit(commitInfo) + } + } + + // + fun withBreakingChanges(block: ChangeLogBuilder.() -> Unit) { + with({ it.isBreaking }, "!", block) + } + + fun withScope(vararg scopes: String, block: ChangeLogBuilder.() -> Unit) { + for (scope in scopes) { + with({ it.scope == scope }, scope, block) + } + } + + fun withType(vararg types: String, block: ChangeLogBuilder.() -> Unit) { + for (type in types) { + with({ it.type == type }, type, block) + } + } + + fun otherwise(block: ChangeLogBuilder.() -> Unit) { + with({ true }, block) + } + + fun with(filter: (ChangeLogFormat.CommitInfo) -> Boolean, block: ChangeLogBuilder.() -> Unit) = + with(filter, null, block) + + fun with( + filter: (ChangeLogFormat.CommitInfo) -> Boolean, + key: String?, + block: ChangeLogBuilder.() -> Unit + ) { + val filteredCommits = commitInfos().filter(filter) + if (filteredCommits.isNotEmpty()) { + val builder = ChangeLogBuilder(key, filteredCommits, context) + builder.block() + append(builder.build()) + } + } + // + + // + fun groupByScope(block: ChangeLogBuilder.() -> Unit) { + groupBySorted({ it.scope }, block) + } + + fun groupByType(block: ChangeLogBuilder.() -> Unit) { + groupBySorted({ it.type }, block) + } + + fun groupBy(keySelector: (ChangeLogFormat.CommitInfo) -> String?, block: ChangeLogBuilder.() -> Unit) { + processGroups(commitInfos().mapNotNull { keyMapper(keySelector, it) } + .groupBy({ it.first }, { it.second }), block) + } + + fun groupBySorted( + keySelector: (ChangeLogFormat.CommitInfo) -> String?, + block: ChangeLogBuilder.() -> Unit + ) { + processGroups(commitInfos().mapNotNull { keyMapper(keySelector, it) } + .groupByTo(TreeMap(), { it.first }, { it.second }), block) + } + + private fun keyMapper( + keySelector: (ChangeLogFormat.CommitInfo) -> String?, + it: ChangeLogFormat.CommitInfo + ): Pair? { + return (keySelector(it) ?: return null) to it + } + + private fun processGroups( + groupedByScope: Map>, + block: ChangeLogBuilder.() -> Unit + ) { + for ((key, scopeCommits) in groupedByScope.filterKeys { it.isNotEmpty() }) { + val builder = ChangeLogBuilder(key, scopeCommits, context) + block(builder) + append(builder.build()) + } + } + // + + private fun commitInfos() = commitInfos.filter { !context.isCommitFlagged(it) } +} + +class ChangeLogTextFormatter( + private val commitInfo: ChangeLogFormat.CommitInfo +) : DocumentBuilder() { + fun header() = (commitInfo.message ?: commitInfo.text).lineSequence().first() + + fun body() = commitInfo.text.lineSequence() + .drop(1) + .dropWhile { it.isEmpty() } + .takeWhile { it.isNotEmpty() } + + fun fullHeader() = commitInfo.text.lineSequence().first() + + fun scope(format: String = "%s: ") = commitInfo.scope?.let { format.format(it) }.orEmpty() + fun type(format: String = "%s: ") = commitInfo.type?.let { format.format(it) }.orEmpty() + + fun hash(format: String = "%s", len: Int = 40) = + commits().joinToString(" ", "", " ") { format.format(it.sha.take(len)) } + + fun commits() = commitInfo.commits +} \ No newline at end of file diff --git a/src/main/kotlin/git/semver/plugin/changelog/ChangeLogDocumentBuilder.kt b/src/main/kotlin/git/semver/plugin/changelog/ChangeLogDocumentBuilder.kt deleted file mode 100644 index f876a88..0000000 --- a/src/main/kotlin/git/semver/plugin/changelog/ChangeLogDocumentBuilder.kt +++ /dev/null @@ -1,116 +0,0 @@ -package git.semver.plugin.changelog - -import java.util.* - -open class ChangeLogDocumentBuilder( - private val context: ChangeLogFormat.Context, - private val commitInfos: List -) : DocumentBuilder() { - - fun format(block: ChangeLogTextFormatter.() -> Unit) { - for (commitInfo in commitInfos()) { - context.flagCommit(commitInfo) - val formatter = ChangeLogTextFormatter(commitInfo) - formatter.block() - appendLine(formatter.build()) - } - } - - fun skip() { - for (commitInfo in commitInfos()) { - context.flagCommit(commitInfo) - } - } - - // - fun withBreakingChanges(block: ChangeLogDocumentBuilder.() -> Unit) { - with({ it.isBreaking }, block) - } - - fun withScope(vararg scopes: String, block: ChangeLogDocumentBuilder.() -> Unit) { - with({ it.scope in scopes }, block) - } - - fun withScope(block: ChangeLogDocumentBuilder.() -> Unit) { - with({ it.scope != null }, block) - } - - fun withType(vararg types: String, block: ChangeLogDocumentBuilder.() -> Unit) { - with({ it.type in types }, block) - } - - fun withType(block: ChangeLogDocumentBuilder.() -> Unit) { - with({ it.type != null }, block) - } - - fun otherwise(block: ChangeLogDocumentBuilder.() -> Unit) { - with({ true }, block) - } - - fun with(filter: (ChangeLogFormat.CommitInfo) -> Boolean, block: ChangeLogDocumentBuilder.() -> Unit) { - val filteredCommits = commitInfos().filter(filter) - if (filteredCommits.isNotEmpty()) { - val builder = ChangeLogDocumentBuilder(context, filteredCommits) - builder.block() - append(builder.build()) - } - } - // - - // - fun groupByScope(block: ChangeLogGroupBuilder.() -> Unit) { - groupBySorted({ it.scope.orEmpty() }, block) - } - - fun groupByType(block: ChangeLogGroupBuilder.() -> Unit) { - groupBySorted({ it.type.orEmpty() }, block) - } - - fun groupBy(keySelector: (ChangeLogFormat.CommitInfo) -> String, block: ChangeLogGroupBuilder.() -> Unit) { - processGroups(commitInfos().groupBy(keySelector), block) - } - - fun groupBySorted(keySelector: (ChangeLogFormat.CommitInfo) -> String, block: ChangeLogGroupBuilder.() -> Unit) { - processGroups(commitInfos().groupByTo(TreeMap(), keySelector), block) - } - - private fun processGroups( - groupedByScope: Map>, - block: ChangeLogGroupBuilder.() -> Unit - ) { - for ((key, scopeCommits) in groupedByScope.filterKeys { it.isNotEmpty() }) { - val builder = ChangeLogGroupBuilder(context, scopeCommits, key) - block(builder) - append(builder.build()) - } - } - // - - private fun commitInfos() = commitInfos.filter { !context.isCommitFlagged(it) } -} - -class ChangeLogGroupBuilder( - context: ChangeLogFormat.Context, - commitInfos: List, - val key: String -) : ChangeLogDocumentBuilder(context, commitInfos) - -class ChangeLogTextFormatter( - private val commitInfo: ChangeLogFormat.CommitInfo -) : DocumentBuilder() { - fun header() = (commitInfo.message ?: commitInfo.text).lineSequence().first() - fun header(transform: (String) -> String) = (commitInfo.message ?: commitInfo.text).lineSequence().first().let(transform) - - fun body() = commitInfo.text.lineSequence() - .drop(1) - .dropWhile { it.isEmpty() } - .takeWhile { it.isNotEmpty() } - - fun fullHeader() = commitInfo.text.lineSequence().first() - - fun scope(format: String = "%s: ") = commitInfo.scope?.let { format.format(it) }.orEmpty() - fun type(format: String = "%s: ") = commitInfo.type?.let { format.format(it) }.orEmpty() - - fun hash(format: String = "%s", len: Int = 40) = - commitInfo.commits.joinToString(" ", "", " ") { format.format(it.sha.take(len)) } -} \ No newline at end of file diff --git a/src/main/kotlin/git/semver/plugin/changelog/ChangeLogFormat.kt b/src/main/kotlin/git/semver/plugin/changelog/ChangeLogFormat.kt index bcb49cb..ceda94d 100644 --- a/src/main/kotlin/git/semver/plugin/changelog/ChangeLogFormat.kt +++ b/src/main/kotlin/git/semver/plugin/changelog/ChangeLogFormat.kt @@ -7,18 +7,22 @@ private const val SCOPE = "Scope" private const val TYPE = "Type" private const val MESSAGE = "Message" +const val HEADER = "#" +const val BREAKING_CHANGE = "!" +const val OTHER_CHANGE = "?" + data class ChangeLogFormat( val groupByText: Boolean = true, val sortByText: Boolean = true, - val builder: ChangeLogDocumentBuilder.() -> Unit + val builder: ChangeLogBuilder.() -> Unit ) { var changeLogPattern = "\\A(?\\w+)(?:\\((?[^()]+)\\))?!?:\\s*(?(?:.|\n)*)" companion object { - var defaultHeader = "## What's Changed" - var defaultBreakingChangeHeader = "### Breaking Changes 🛠" - var defaultOtherChangeHeader = "### Other Changes \uD83D\uDCA1" val defaultHeaderTexts = mutableMapOf( + HEADER to "## What's Changed", + BREAKING_CHANGE to "### Breaking Changes 🛠", + OTHER_CHANGE to "### Other Changes \uD83D\uDCA1", "fix" to "### Bug Fixes \uD83D\uDC1E", "feat" to "### New Features \uD83C\uDF89", "test" to "### Tests ✅", @@ -32,33 +36,33 @@ data class ChangeLogFormat( ) val defaultChangeLog = ChangeLogFormat { - appendLine(defaultHeader).appendLine() + appendLine(defaultHeaderTexts[HEADER]).appendLine() withType("release") { skip() } withBreakingChanges { - appendLine(defaultBreakingChangeHeader) - format { + appendLine(defaultHeaderTexts[BREAKING_CHANGE]) + formatChanges { append("- ").append(hash()).appendLine(fullHeader()) } appendLine() } - groupBySorted({ defaultHeaderTexts[it.scope] ?: defaultHeaderTexts[it.type].orEmpty() }) { - appendLine(key) + groupBySorted({ defaultHeaderTexts[it.scope] ?: defaultHeaderTexts[it.type] }) { + appendLine(groupKey) with({ defaultHeaderTexts.containsKey(it.scope) }) { - format { + formatChanges { append("- ").append(hash()).append(type()).appendLine(header()) } } - format { + formatChanges { append("- ").append(hash()).append(scope()).appendLine(header()) } appendLine() } otherwise { - appendLine(defaultOtherChangeHeader) - format { + appendLine(defaultHeaderTexts[OTHER_CHANGE]) + formatChanges { appendLine("- ${hash()}${fullHeader()}") } appendLine() @@ -66,25 +70,18 @@ data class ChangeLogFormat( } val simpleChangeLog = ChangeLogFormat { - appendLine(defaultHeader).appendLine() + appendLine(defaultHeaderTexts[HEADER]).appendLine() withBreakingChanges { - appendLine(defaultBreakingChangeHeader) - format { + appendLine(defaultHeaderTexts[BREAKING_CHANGE]) + formatChanges { append("- ").appendLine(fullHeader()) } appendLine() } - withType("fix") { - appendLine(defaultHeaderTexts["fix"]) - format { - append("- ").append(scope()).appendLine(header()) - } - appendLine() - } - withType("feat") { - appendLine(defaultHeaderTexts["feat"]) - format { + withType("fix", "feat") { + appendLine(defaultHeaderTexts[groupKey]) + formatChanges { append("- ").append(scope()).appendLine(header()) } appendLine() @@ -92,28 +89,28 @@ data class ChangeLogFormat( } val scopeChangeLog = ChangeLogFormat { - appendLine(defaultHeader).appendLine() + appendLine(defaultHeaderTexts[HEADER]).appendLine() withType("release") { skip() } - withBreakingChanges(formatGroupByScopeDisplayType(defaultBreakingChangeHeader)) - groupBySorted({ defaultHeaderTexts[it.type].orEmpty() }, formatGroupByScope()) - otherwise (formatGroupByScopeDisplayType(defaultOtherChangeHeader)) + withBreakingChanges(formatGroupByScopeDisplayType(defaultHeaderTexts[BREAKING_CHANGE])) + groupBySorted({ defaultHeaderTexts[it.type] }, formatGroupByScope()) + otherwise (formatGroupByScopeDisplayType(defaultHeaderTexts[OTHER_CHANGE])) } - private fun formatGroupByScope(): ChangeLogGroupBuilder.() -> Unit = { - appendLine(key).appendLine() + private fun formatGroupByScope(): ChangeLogBuilder.() -> Unit = { + appendLine(groupKey).appendLine() groupByScope { - append("#### ").appendLine(key) - format { + append("#### ").appendLine(groupKey) + formatChanges { append("- ").append(hash()).appendLine(header()) } appendLine() } otherwise { appendLine("#### Missing scope") - format { + formatChanges { append("- ").append(hash()).appendLine(header()) } appendLine() @@ -121,18 +118,18 @@ data class ChangeLogFormat( appendLine() } - private fun formatGroupByScopeDisplayType(header: String): ChangeLogDocumentBuilder.() -> Unit = { + private fun formatGroupByScopeDisplayType(header: String?): ChangeLogBuilder.() -> Unit = { appendLine(header).appendLine() groupByScope { - append("#### ").appendLine(key) - format { + append("#### ").appendLine(groupKey) + formatChanges { append("- ").append(hash()).append(type()).appendLine(header()) } appendLine() } otherwise { appendLine("#### Missing scope") - format { + formatChanges { append("- ").append(hash()).append(type()).appendLine(header()) } appendLine() @@ -142,10 +139,11 @@ data class ChangeLogFormat( } fun formatLog(changeLog: List, settings: SemverSettings): String { + val context = Context() val commitInfos = getCommitInfos(changeLog, settings) - val changeLogDocumentBuilder = ChangeLogDocumentBuilder(Context(), commitInfos) - changeLogDocumentBuilder.builder() - return changeLogDocumentBuilder.build() + val changeLogBuilder = ChangeLogBuilder(HEADER, commitInfos, context) + changeLogBuilder.builder() + return changeLogBuilder.build() } private fun getCommitInfos(changeLog: List, settings: SemverSettings): List { @@ -172,6 +170,7 @@ data class ChangeLogFormat( commits, text, isBreakingChange, + true, it.groupValue(TYPE), it.groupValue(SCOPE), it.groupValue(MESSAGE) @@ -181,10 +180,11 @@ data class ChangeLogFormat( private fun MatchResult.groupValue(groupId: String) = groups[groupId]?.value - class CommitInfo( + data class CommitInfo( val commits: List, val text: String, val isBreaking: Boolean, + val isChangelogPatternMatch: Boolean = false, val type: String? = null, val scope: String? = null, val message: String? = null diff --git a/src/main/kotlin/git/semver/plugin/changelog/DocumentBuilder.kt b/src/main/kotlin/git/semver/plugin/changelog/DocumentBuilder.kt index 9292870..5b7ef41 100644 --- a/src/main/kotlin/git/semver/plugin/changelog/DocumentBuilder.kt +++ b/src/main/kotlin/git/semver/plugin/changelog/DocumentBuilder.kt @@ -16,34 +16,4 @@ open class DocumentBuilder { out.appendLine(t) return this } - - fun appendHeading1Line(text: String): DocumentBuilder { - out.append("# ").appendLine(text) - return this - } - - fun appendHeading2Line(text: String): DocumentBuilder { - out.append("## ").appendLine(text) - return this - } - - fun appendHeading3Line(text: String): DocumentBuilder { - out.append("### ").appendLine(text) - return this - } - - fun appendHeading4Line(text: String): DocumentBuilder { - out.append("#### ").appendLine(text) - return this - } - - fun appendBold(text: String): DocumentBuilder { - out.append("**").append(text).append("**") - return this - } - - fun appendItalic(text: String): DocumentBuilder { - out.append("_").append(text).append("_") - return this - } } \ No newline at end of file diff --git a/src/main/kotlin/git/semver/plugin/gradle/GitSemverPluginExtension.kt b/src/main/kotlin/git/semver/plugin/gradle/GitSemverPluginExtension.kt index 37d4e2a..6de2e45 100644 --- a/src/main/kotlin/git/semver/plugin/gradle/GitSemverPluginExtension.kt +++ b/src/main/kotlin/git/semver/plugin/gradle/GitSemverPluginExtension.kt @@ -1,6 +1,6 @@ package git.semver.plugin.gradle -import git.semver.plugin.changelog.ChangeLogDocumentBuilder +import git.semver.plugin.changelog.ChangeLogBuilder import git.semver.plugin.changelog.ChangeLogFormat import git.semver.plugin.scm.GitProvider import git.semver.plugin.semver.SemverSettings @@ -17,7 +17,7 @@ open class GitSemverPluginExtension(project: Project) : SemverSettings() { val infoVersion by lazy { semVersion.toInfoVersionString() } var changeLogFormat = ChangeLogFormat.defaultChangeLog - fun changeLogFormat(builder: ChangeLogDocumentBuilder.() -> Unit) { + fun changeLogFormat(builder: ChangeLogBuilder.() -> Unit) { changeLogFormat = ChangeLogFormat(builder = builder) } val changeLogList by lazy { GitProvider(this).getChangeLog(gitDirectory) }