From f7ef0b28ab7b2c0ec31168bbdfab31e961909606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Irland?= Date: Tue, 16 Apr 2019 19:25:54 -0300 Subject: [PATCH] Add history section in the release note config and add the ability to set a history start point (#25) --- CHANGELOG.md | 7 ++ README.md | 65 +++++++++++++---- .../xmartlabs/snapshotpublisher/Constants.kt | 6 +- .../model/ReleaseNotesConfig.kt | 18 +++-- .../task/GenerateReleaseNotesTask.kt | 34 ++++++--- .../snapshotpublisher/utils/GitHelper.kt | 19 +++-- .../utils/ReleaseNotesGenerator.kt | 15 +++- .../snapshotpublisher/ReleaseNotesTest.kt | 72 +++++++++++++++---- 8 files changed, 184 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e442b10..ebe768d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. [Version 1.0.1 _(Pending release)_]() --- +### New Features +- Added new `history` variable to use in `releaseNotesFormat` given by `historyFormat`. +It's shown only if the `{commitHistory}` is not empty. +- Added `includeHistorySinceLastTag` release notes setup variable. +It enables to generate the history only for the commits after the latest git's tag. +It's useful to show only the changes that changed since the last build. (#25) + ### Fixes - Fix version name issue in generated release notes. (24) diff --git a/README.md b/README.md index f734b30..b12b929 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,22 @@ Currently the available services are: - [Google Play](https://play.google.com/apps/publish/) - [Fabric Beta](https://docs.fabric.io/apple/beta/overview.html) +## Table of content + +- [Installation](#installation) + - [Setup](#setup) + - [Version customization](#version-customization) + - [Release notes](#release-notes) + - [Version](#version) + - [Header](#header) + - [History](#history) + - [Other](#other) + - [Fabric Beta](#fabric-beta) + - [Google Play](#google-play) +- [How to use it?](#how-to-use-it) +- [Getting involved](#getting-involved) +- [About](#about) + ## Installation The plugin is hosted in the Gradle Plugin Portal. @@ -121,55 +137,76 @@ All fields in that block are optional and their default values are: snapshotPublisher { releaseNotes { releaseNotesFormat = """{version}: {header} - -Last Changes: -{commitHistory} +{history} """ versionFormat = '{versionName}' headerFormat = '%s%n%nAuthor: %an <%ae>' + historyFormat = '\nLast Changes:\n{commitHistory}' commitHistoryFormat = '• %s (%an - %ci)' maxCommitHistoryLines = 10 - outputFile = null includeLastCommitInHistory = false includeMergeCommitsInHistory = true + includeHistorySinceLastTag = false + outputFile = null } // ... } ``` -> Note: you can test the generated release notes executing the `generateSnapshotReleaseNotes` gradle task. +> Note: you can test the generated release notes executing the `generateSnapshotReleaseNotes` gradle task and if you also add the `--info` flag you will see them in the command's output. - `releaseNotesFormat`: Defines the format of the release notes: The possible variables to play with in this case are: - - `{version}` given by `versionFormat`. - - `{header}` given by `headerFormat`. + - [`{version}`](#version) given by `versionFormat`. + - [`{header}`](#header) given by `headerFormat`. By default it contains information about the most recent commit and their author. - - `{commitHistory}` given by the result of apply `commitHistoryFormat` to a range of commits. - By default the range includes all commits from the last -not current- commit to `maxCommitHistoryLines` commits before that. - If you want to include the last commit in that range, you can set `includeLastCommitInHistory` as `true`. + - [`{history}`](#history) given by `historyFormat`. + It contains information about the most recent history of the build. +#### Version +It contains information about the build version. - `versionFormat`: Specifies the version's variable format. `{versionName}` (Android app's Version Name) and `{versionCode}` (Android app's Version Code) can be used to create it. +#### Header +It contains information about the most recent commit. + - `headerFormat`: Specifies the header's variable format. The plugin uses [Git's pretty format] to retrieve the information about the current commit. If you want to modify this, you may want to use it. +#### History +It contains information about the most recent commits, starting from the last -not current- commit to `maxCommitHistoryLines` commits before that. +If you want to include the last commit in that range, you can set `includeLastCommitInHistory` as `true`. +The format of these commits is given by `commitHistoryFormat`. +This section is shown only if there is at least one commit history. + +- `historyFormat`: Specifies the history's variable format. +It contains the `{commitHistory}`, which is given by the result of applying `commitHistoryFormat` to a certain range of commits of commits. + - `commitHistoryFormat`: Specifies the `{commitHistory}` variable format. As `headerFormat` does, it uses [Git's pretty format] to create the `commitHistory` for the previous commits. - `maxCommitHistoryLines`: Indicates the number of commits included in `{commitHistory}`. +- `includeLastCommitInHistory`: Flag to include the most recent commit in `{commitHistory}`. +By default this value is `false` because it's used in `{header}` + +- `includeMergeCommitsInHistory`: Flag to include merge commits in `{commitHistory}`. + +- `includeHistorySinceLastTag`: Indicates the history start point. +By default it's `false`, which means that all commits are used to get the history. +However, if its value is `true`, only the commits after the most recent tag will be included in the build history. +This is useful if you want to include in the history only the changes after the previous release. + +#### Other + - `outputFile`: The file where the release notes will be saved. By default this value is `null` and that means the release notes will be generated and delivered with the snapshot build but it will not be saved in the file system. If you want to save the release notes in the file system, you can set `outputFile = file("release-notes.txt")`. You can define it using a relative path (where the start point is the Android application module folder) or an absolute path. -- `includeLastCommitInHistory`: Flag to include the most recent commit in `{commitHistory}`. -By default this value is `false` because it's used in `{header}` - -- `includeMergeCommitsInHistory`: Flag to include merge commits in `{commitHistory}`. ### Fabric Beta This block defines the configuration needed to deploy the artifacts in Fabric's Beta. diff --git a/src/main/kotlin/com/xmartlabs/snapshotpublisher/Constants.kt b/src/main/kotlin/com/xmartlabs/snapshotpublisher/Constants.kt index 6384929..ae00026 100644 --- a/src/main/kotlin/com/xmartlabs/snapshotpublisher/Constants.kt +++ b/src/main/kotlin/com/xmartlabs/snapshotpublisher/Constants.kt @@ -28,6 +28,7 @@ internal object Constants { const val VERSION_FORMAT = "$VERSION_FORMAT_CURRENT_VERSION_NAME_KEY-$VERSION_FORMAT_COMMIT_HASH_KEY" const val RELEASE_NOTES_COMMIT_HISTORY_KEY = "{commitHistory}" + const val RELEASE_NOTES_HISTORY_KEY = "{history}" const val RELEASE_NOTES_HEADER_KEY = "{header}" const val RELEASE_NOTES_VERSION_CODE_KEY = "{versionCode}" const val RELEASE_NOTES_VERSION_KEY = "{version}" @@ -36,15 +37,16 @@ internal object Constants { const val RELEASE_NOTES_VERSION_FORMAT_DEFAULT_VALUE = RELEASE_NOTES_VERSION_NAME_KEY const val RELEASE_NOTES_CONFIG_COMMIT_HISTORY_FORMAT_DEFAULT_VALUE = "• %s (%an - %ci)" const val RELEASE_NOTES_CONFIG_HEADER_FORMAT_DEFAULT_VALUE = "%s%n%nAuthor: %an <%ae>" + const val RELEASE_NOTES_CONFIG_HISTORY_FORMAT_DEFAULT_VALUE = "Last Changes:\n$RELEASE_NOTES_COMMIT_HISTORY_KEY" const val RELEASE_NOTES_CONFIG_MAX_COMMIT_HISTORY_LINES_DEFAULT_VALUE = 10 const val RELEASE_NOTES_INCLUDE_MERGE_COMMITS_DEFAULT_VALUE = true const val RELEASE_NOTES_INCLUDE_LAST_COMMIT_DEFAULT_VALUE = false + const val RELEASE_NOTES_INCLUDE_HISTORY_ONLY_FROM_PREVIOUS_TAG_DEFAULT_VALUE = false val RELEASE_NOTES_OUTPUT_FILE_DEFAULT_VALUE: File? = null const val RELEASE_NOTES_CONFIG_FORMAT_DEFAULT_VALUE = """$RELEASE_NOTES_VERSION_KEY: $RELEASE_NOTES_HEADER_KEY -Last Changes: -$RELEASE_NOTES_COMMIT_HISTORY_KEY +$RELEASE_NOTES_HISTORY_KEY """ const val FABRIC_DEPLOY_DISTRIBUTION_EMAILS_DEFAULT_VALUE = "" diff --git a/src/main/kotlin/com/xmartlabs/snapshotpublisher/model/ReleaseNotesConfig.kt b/src/main/kotlin/com/xmartlabs/snapshotpublisher/model/ReleaseNotesConfig.kt index 6e0f774..668bc9b 100644 --- a/src/main/kotlin/com/xmartlabs/snapshotpublisher/model/ReleaseNotesConfig.kt +++ b/src/main/kotlin/com/xmartlabs/snapshotpublisher/model/ReleaseNotesConfig.kt @@ -8,19 +8,27 @@ open class ReleaseNotesConfig { var versionFormat: String = Constants.RELEASE_NOTES_VERSION_FORMAT_DEFAULT_VALUE var headerFormat: String = Constants.RELEASE_NOTES_CONFIG_HEADER_FORMAT_DEFAULT_VALUE var commitHistoryFormat: String = Constants.RELEASE_NOTES_CONFIG_COMMIT_HISTORY_FORMAT_DEFAULT_VALUE + var historyFormat: String = Constants.RELEASE_NOTES_CONFIG_HISTORY_FORMAT_DEFAULT_VALUE var maxCommitHistoryLines: Int = Constants.RELEASE_NOTES_CONFIG_MAX_COMMIT_HISTORY_LINES_DEFAULT_VALUE @Suppress("MemberVisibilityCanBePrivate") var releaseNotesFormat: String = Constants.RELEASE_NOTES_CONFIG_FORMAT_DEFAULT_VALUE var outputFile: File? = Constants.RELEASE_NOTES_OUTPUT_FILE_DEFAULT_VALUE var includeMergeCommitsInHistory: Boolean = Constants.RELEASE_NOTES_INCLUDE_MERGE_COMMITS_DEFAULT_VALUE var includeLastCommitInHistory: Boolean = Constants.RELEASE_NOTES_INCLUDE_LAST_COMMIT_DEFAULT_VALUE + var includeHistorySinceLastTag: Boolean = + Constants.RELEASE_NOTES_INCLUDE_HISTORY_ONLY_FROM_PREVIOUS_TAG_DEFAULT_VALUE + + internal fun getHistory(commitHistory: String): String = historyFormat + .replace(Constants.RELEASE_NOTES_COMMIT_HISTORY_KEY, commitHistory, true) + + internal fun getReleaseNotes(version: String, header: String, history: String, changelog: String): String = + releaseNotesFormat + .replace(Constants.RELEASE_NOTES_VERSION_KEY, version, true) + .replace(Constants.RELEASE_NOTES_HEADER_KEY, header, true) + .replace(Constants.RELEASE_NOTES_HISTORY_KEY, history, true) + .replace(Constants.RELEASE_NOTES_COMMIT_HISTORY_KEY, changelog, true) internal fun getVersion(versionName: String, versionCode: Int) = versionFormat .replace(Constants.RELEASE_NOTES_VERSION_NAME_KEY, versionName, true) .replace(Constants.RELEASE_NOTES_VERSION_CODE_KEY, versionCode.toString(), true) - - internal fun getReleaseNotes(version: String, header: String, changelog: String): String = releaseNotesFormat - .replace(Constants.RELEASE_NOTES_VERSION_KEY, version, true) - .replace(Constants.RELEASE_NOTES_HEADER_KEY, header, true) - .replace(Constants.RELEASE_NOTES_COMMIT_HISTORY_KEY, changelog, true) } diff --git a/src/main/kotlin/com/xmartlabs/snapshotpublisher/task/GenerateReleaseNotesTask.kt b/src/main/kotlin/com/xmartlabs/snapshotpublisher/task/GenerateReleaseNotesTask.kt index cd84954..0a402e8 100644 --- a/src/main/kotlin/com/xmartlabs/snapshotpublisher/task/GenerateReleaseNotesTask.kt +++ b/src/main/kotlin/com/xmartlabs/snapshotpublisher/task/GenerateReleaseNotesTask.kt @@ -2,6 +2,7 @@ package com.xmartlabs.snapshotpublisher.task import com.android.build.gradle.api.ApplicationVariant import com.xmartlabs.snapshotpublisher.Constants +import com.xmartlabs.snapshotpublisher.model.ReleaseNotesConfig import com.xmartlabs.snapshotpublisher.plugin.AndroidPluginHelper import com.xmartlabs.snapshotpublisher.utils.ReleaseNotesGenerator import com.xmartlabs.snapshotpublisher.utils.snapshotReleaseExtension @@ -18,6 +19,12 @@ open class GenerateReleaseNotesTask : DefaultTask() { @TaskAction fun action() { val releaseNotesConfig = project.snapshotReleaseExtension.releaseNotes + + if (releaseNotesConfig.releaseNotesFormat.contains(Constants.RELEASE_NOTES_COMMIT_HISTORY_KEY)) { + project.logger.warn("${Constants.RELEASE_NOTES_COMMIT_HISTORY_KEY} in `releaseNotesFormat` is deprecated and " + + "it will remove in the next major update") + } + val generatedReleaseNotes = ReleaseNotesGenerator.generate( releaseNotesConfig = releaseNotesConfig, versionName = AndroidPluginHelper.getVersionName(project, variant), @@ -26,16 +33,21 @@ open class GenerateReleaseNotesTask : DefaultTask() { project.logger.info("Generated Release Notes: \n$generatedReleaseNotes") - val releaseNotesFile = File(project.buildDir, Constants.OUTPUT_RELEASE_NOTES_FILE_PATH) - .apply { - parentFile.mkdirs() - writeText(generatedReleaseNotes) - } - - releaseNotesConfig.outputFile - ?.apply { - parentFile.mkdirs() - releaseNotesFile.copyTo(target = this, overwrite = true) - } + val releaseNotesFile = createGeneratedReleaseNotesFile(generatedReleaseNotes) + copyGeneratedReleaseNoteFileToOutputFile(releaseNotesConfig, releaseNotesFile) } + + private fun copyGeneratedReleaseNoteFileToOutputFile(releaseNotesConfig: ReleaseNotesConfig, releaseNotesFile: File) = + releaseNotesConfig.outputFile + ?.apply { + parentFile.mkdirs() + releaseNotesFile.copyTo(target = this, overwrite = true) + } + + private fun createGeneratedReleaseNotesFile(generatedReleaseNotes: String) = + File(project.buildDir, Constants.OUTPUT_RELEASE_NOTES_FILE_PATH) + .apply { + parentFile.mkdirs() + writeText(generatedReleaseNotes) + } } diff --git a/src/main/kotlin/com/xmartlabs/snapshotpublisher/utils/GitHelper.kt b/src/main/kotlin/com/xmartlabs/snapshotpublisher/utils/GitHelper.kt index 1816a57..83b5653 100644 --- a/src/main/kotlin/com/xmartlabs/snapshotpublisher/utils/GitHelper.kt +++ b/src/main/kotlin/com/xmartlabs/snapshotpublisher/utils/GitHelper.kt @@ -25,6 +25,8 @@ internal object GitHelper { return proc.inputStream.bufferedReader().readText().trim() } + private fun getLatestTag() = "git rev-list --tags --max-count=1".execute() + fun getCommitHash() = "git rev-parse --short HEAD".execute() fun getBranchName() = "git rev-parse --abbrev-ref HEAD".execute() @@ -32,16 +34,23 @@ internal object GitHelper { fun getLog(format: String, numberOfCommits: Int = Int.MAX_VALUE) = "git log --pretty=format:'$format' -n $numberOfCommits".execute() - private fun getTotalNumberOfCommits(from: String = "HEAD", commandArg: String?) = - "git rev-list --count $from ${commandArg ?: ""}".execute().toInt() + private fun getTotalNumberOfCommits(logRange: String, commandArg: String?) = + "git rev-list --count $logRange ${commandArg ?: ""}".execute().toInt() + + private fun getHistoryRange(releaseNotesConfig: ReleaseNotesConfig): String { + val startRange = if (releaseNotesConfig.includeHistorySinceLastTag) getLatestTag() else null + val endRange = "HEAD" + if (releaseNotesConfig.includeLastCommitInHistory) "" else "^" + return if (startRange.isNullOrBlank()) endRange else "$startRange..$endRange" + } fun getHistoryFromPreviousCommit(releaseNotesConfig: ReleaseNotesConfig): String { with(releaseNotesConfig) { val allowMergeCommitCommandArg = if (includeMergeCommitsInHistory) "" else "--no-merges" - val logStartCommand = "HEAD" + if (includeLastCommitInHistory) "" else "^" - val numberOfCommits = GitHelper.getTotalNumberOfCommits(logStartCommand, allowMergeCommitCommandArg) + + val logRange = getHistoryRange(this) + val numberOfCommits = GitHelper.getTotalNumberOfCommits(logRange, allowMergeCommitCommandArg) val requireCommitsCommandArg = if (numberOfCommits <= maxCommitHistoryLines) "" else " -n $maxCommitHistoryLines" - val gitLogCommand = "git log --pretty=format:'$commitHistoryFormat'$requireCommitsCommandArg $logStartCommand" + val gitLogCommand = "git log --pretty=format:'$commitHistoryFormat'$requireCommitsCommandArg $logRange" return "$gitLogCommand $allowMergeCommitCommandArg".execute() } diff --git a/src/main/kotlin/com/xmartlabs/snapshotpublisher/utils/ReleaseNotesGenerator.kt b/src/main/kotlin/com/xmartlabs/snapshotpublisher/utils/ReleaseNotesGenerator.kt index 6e7b382..7965266 100644 --- a/src/main/kotlin/com/xmartlabs/snapshotpublisher/utils/ReleaseNotesGenerator.kt +++ b/src/main/kotlin/com/xmartlabs/snapshotpublisher/utils/ReleaseNotesGenerator.kt @@ -7,8 +7,14 @@ internal object ReleaseNotesGenerator { fun generate(releaseNotesConfig: ReleaseNotesConfig, versionName: String, versionCode: Int): String { val version = getVersionSection(releaseNotesConfig, versionName, versionCode) val header = getHeaderSection(releaseNotesConfig) - val changelog = getHistorySection(releaseNotesConfig) - return releaseNotesConfig.getReleaseNotes(version = version, header = header, changelog = changelog) + val history = getHistorySection(releaseNotesConfig) + val changelog = getChangelogSection(releaseNotesConfig) + return releaseNotesConfig.getReleaseNotes( + version = version, + header = header, + history = history, + changelog = changelog + ) } fun truncateReleaseNotesIfNeeded(releaseNotes: String, maxCharacters: Int): String { @@ -41,5 +47,10 @@ internal object ReleaseNotesGenerator { @VisibleForTesting internal fun getHistorySection(releaseNotesConfig: ReleaseNotesConfig) = + getChangelogSection(releaseNotesConfig).let { changelog -> + if (changelog.isBlank()) "" else releaseNotesConfig.getHistory(changelog) + } + + private fun getChangelogSection(releaseNotesConfig: ReleaseNotesConfig) = GitHelper.getHistoryFromPreviousCommit(releaseNotesConfig) } diff --git a/src/test/kotlin/com/xmartlabs/snapshotpublisher/ReleaseNotesTest.kt b/src/test/kotlin/com/xmartlabs/snapshotpublisher/ReleaseNotesTest.kt index 400054a..f91075d 100644 --- a/src/test/kotlin/com/xmartlabs/snapshotpublisher/ReleaseNotesTest.kt +++ b/src/test/kotlin/com/xmartlabs/snapshotpublisher/ReleaseNotesTest.kt @@ -25,6 +25,7 @@ class ReleaseNotesTest { private val REPO_FOLDER = File("/tmp/${UUID.randomUUID()}/") private val REPO_GIT_FOLDER = File("${REPO_FOLDER.absoluteFile}/.git") private val COMMIT_TIMEZONE = TimeZone.getTimeZone("GMT-3:00") + private const val TAG_POSITION = NUMBER_OF_COMMITS - 3 private val COMMIT_DATE = Calendar.getInstance() .apply { timeZone = COMMIT_TIMEZONE @@ -45,15 +46,13 @@ class ReleaseNotesTest { data class Author(val name: String, val mail: String) - private fun Repository.addCommit(commit: Commit) { - Git(this) - .commit() - .setMessage(commit.message) - .setAuthor(commit.author.name, commit.author.mail) - .setCommitter(PersonIdent(commit.author.name, commit.author.mail, COMMIT_DATE, COMMIT_TIMEZONE)) - .setAllowEmpty(true) - .call() - } + private fun Repository.addCommit(commit: Commit) = Git(this) + .commit() + .setMessage(commit.message) + .setAuthor(commit.author.name, commit.author.mail) + .setCommitter(PersonIdent(commit.author.name, commit.author.mail, COMMIT_DATE, COMMIT_TIMEZONE)) + .setAllowEmpty(true) + .call() @Before fun setup() { @@ -61,8 +60,15 @@ class ReleaseNotesTest { REPO_GIT_FOLDER ) newlyCreatedRepo.create() - COMMITS.forEach { newlyCreatedRepo.addCommit(it) } + val commits = COMMITS.map { newlyCreatedRepo.addCommit(it) } GitHelper.defaultDir = REPO_FOLDER + + Git(newlyCreatedRepo).tag() + .apply { + name = "test.tag" + objectId = commits[TAG_POSITION] + call() + } } @After @@ -101,6 +107,30 @@ class ReleaseNotesTest { checkChangelogIsRight(config) } + @Test + fun `Test empty history section`() { + val config = ReleaseNotesConfig() + .apply { + commitHistoryFormat = "- %s" + maxCommitHistoryLines = 0 + historyFormat = "Test {commitHistory}" + } + val libraryHistory = ReleaseNotesGenerator.getHistorySection(config) + assertEquals("", libraryHistory) + } + + @Test + fun `Test history section with custom format`() { + val config = ReleaseNotesConfig() + .apply { + historyFormat = "Changelog: \n{commitHistory}" + commitHistoryFormat = "- %s" + maxCommitHistoryLines = 5 + } + + checkChangelogIsRight(config, "Changelog: \n%s") + } + @Test fun `Test history section with less commits that are required`() { val config = ReleaseNotesConfig() @@ -174,6 +204,18 @@ class ReleaseNotesTest { assertEquals(expectedReleaseNotes, generatedReleaseNotes) } + @Test + fun `Test release notes from previous tag`() { + val config = ReleaseNotesConfig() + .apply { + commitHistoryFormat = "- %s" + maxCommitHistoryLines = NUMBER_OF_COMMITS + includeLastCommitInHistory = true + } + + checkChangelogIsRight(config) + } + @Test fun `Test default values`() { val generatedReleaseNotes = ReleaseNotesGenerator.generate(ReleaseNotesConfig(), "1.1.1", 12) @@ -197,15 +239,19 @@ Last Changes: assertEquals(expectedReleaseNotes, generatedReleaseNotes) } - private fun checkChangelogIsRight(config: ReleaseNotesConfig) { + private fun checkChangelogIsRight(config: ReleaseNotesConfig, historyStringFormat: String = "Last Changes:\n%s") { val libraryHistory = ReleaseNotesGenerator.getHistorySection(config) val lastCommit = COMMITS.size + if (config.includeLastCommitInHistory) 0 else -1 - val realHistory = COMMITS.subList(Math.max(COMMITS.size - config.maxCommitHistoryLines - 1, 0), lastCommit) + var initialCommitIndex = Math.max(COMMITS.size - config.maxCommitHistoryLines - 1, 0) + if (config.includeHistorySinceLastTag) { + initialCommitIndex = Math.min(TAG_POSITION, initialCommitIndex) + } + val realHistory = COMMITS.subList(initialCommitIndex, lastCommit) .reversed() .joinToString("\n") { config.commitHistoryFormat.format(it.message) } - assertEquals(libraryHistory, realHistory) + assertEquals(libraryHistory, historyStringFormat.format(realHistory)) } }