From beef14b25ea7a837018a5b36f355b6308b4237d6 Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Fri, 29 Jan 2016 01:50:31 +0100 Subject: [PATCH] Added bumpversion command. Issue #160. --- CHANGES.rst | 5 + README.rst | 5 + doc/source/entrypoints.rst | 48 +++++++ doc/source/options.rst | 6 + setup.py | 4 + zest/releaser/bumpversion.py | 152 ++++++++++++++++++++++ zest/releaser/preparedocs.py | 2 + zest/releaser/prerelease.py | 2 +- zest/releaser/tests/addchangelogentry.txt | 6 +- zest/releaser/tests/bumpversion.txt | 94 +++++++++++++ zest/releaser/tests/utils.txt | 2 +- zest/releaser/utils.py | 12 +- 12 files changed, 329 insertions(+), 9 deletions(-) create mode 100644 zest/releaser/bumpversion.py create mode 100644 zest/releaser/tests/bumpversion.txt diff --git a/CHANGES.rst b/CHANGES.rst index ee6afe5b..35a396f8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,11 @@ Changelog for zest.releaser 6.6 (unreleased) ---------------- +- Added ``bumpversion`` command. Options ``--feature`` and + ``--breaking``. Issue #160. The exact behavior might change in + future versions after more practical experience. Try it out and + report any issues you find. [maurits] + - Fixed possible encoding problems when writing files. This is especially for an ascii file to which we add non ascii characters, like in the ``addchangelogentry`` command. [maurits] diff --git a/README.rst b/README.rst index 7b1d9ea9..b4a3bf24 100644 --- a/README.rst +++ b/README.rst @@ -202,3 +202,8 @@ There are some additional tools: is indented and the first line is started with a dash. The command detects it if you use for example a star as first character of an entry. + +- **bumpversion**: do not release, only bump the version. A + development marker is kept when it is there. With ``--feature`` we + update the minor version. With option ``--breaking`` we update the + major version. diff --git a/doc/source/entrypoints.rst b/doc/source/entrypoints.rst index 266c3f73..acf83f54 100644 --- a/doc/source/entrypoints.rst +++ b/doc/source/entrypoints.rst @@ -274,3 +274,51 @@ reporoot workingdir Original working directory + +Bumpversion data dict items +--------------------------- + +breaking + True if we handle a breaking (major) change + +commit_msg + Message template used when committing. + +feature + True if we handle a feature (minor) change + +headings + Extracted headings from the history file + +history_encoding + The detected encoding of the history file + +history_file + Filename of history/changelog file (when found) + +history_header + Header template used for 1st history header + +history_insert_line_here + Line number where an extra changelog entry can be inserted. + +history_last_release + Full text of all history entries of the current release + +history_lines + List with all history file lines (when found) + +name + Name of the project being released + +new_version + New development version (so 1.1) + +release + Type of release: breaking, feature, normal + +reporoot + Root of the version control repository + +workingdir + Original working directory diff --git a/doc/source/options.rst b/doc/source/options.rst index 448ffde1..71ae600e 100644 --- a/doc/source/options.rst +++ b/doc/source/options.rst @@ -44,6 +44,12 @@ Or on multiple lines:: This was difficult." +The ``bumpversion`` command accepts two mutually exclusive options: + +- With ``--feature`` we update the minor version. + +- With option ``--breaking`` we update the major version. + Global options -------------- diff --git a/setup.py b/setup.py index 3d11286f..08830aad 100644 --- a/setup.py +++ b/setup.py @@ -82,6 +82,7 @@ def read(filename): 'lasttagdiff = zest.releaser.lasttagdiff:main', 'lasttaglog = zest.releaser.lasttaglog:main', 'addchangelogentry = zest.releaser.addchangelogentry:main', + 'bumpversion = zest.releaser.bumpversion:main', ], # The datachecks are implemented as entry points to be able to check # our entry point implementation. @@ -97,6 +98,9 @@ def read(filename): 'zest.releaser.addchangelogentry.middle': [ 'datacheck = zest.releaser.addchangelogentry:datacheck', ], + 'zest.releaser.bumpversion.middle': [ + 'datacheck = zest.releaser.bumpversion:datacheck', + ], # Documentation generation 'zest.releaser.prereleaser.before': [ 'preparedocs = ' + diff --git a/zest/releaser/bumpversion.py b/zest/releaser/bumpversion.py new file mode 100644 index 00000000..1b2d4eb0 --- /dev/null +++ b/zest/releaser/bumpversion.py @@ -0,0 +1,152 @@ +"""Do the checks and tasks that have to happen after doing a release. +""" +from __future__ import unicode_literals + +import logging +import sys + +from zest.releaser import baserelease +from zest.releaser import utils + +logger = logging.getLogger(__name__) + +HISTORY_HEADER = '%(new_version)s (unreleased)' +COMMIT_MSG = 'Bumped version for %(release)s release.' + +DATA = { + # Documentation for self.data. You get runtime warnings when something is + # in self.data that is not in this list. Embarrasment-driven + # documentation! + 'workingdir': 'Original working directory', + 'reporoot': 'Root of the version control repository', + 'name': 'Name of the project being released', + 'new_version': 'New development version (so 1.1)', + 'commit_msg': 'Message template used when committing.', + 'headings': 'Extracted headings from the history file', + 'history_file': 'Filename of history/changelog file (when found)', + 'history_last_release': ( + 'Full text of all history entries of the current release'), + 'history_header': 'Header template used for 1st history header', + 'history_lines': 'List with all history file lines (when found)', + 'history_encoding': 'The detected encoding of the history file', + 'history_insert_line_here': ( + 'Line number where an extra changelog entry can be inserted.'), + 'original_version': 'Version before bump (e.g. 1.0.dev0)', + 'breaking': 'True if we handle a breaking (major) change', + 'feature': 'True if we handle a feature (minor) change', + 'release': 'Type of release: breaking, feature, normal', +} + + +class BumpVersion(baserelease.Basereleaser): + """Add a changelog entry. + + self.data holds data that can optionally be changed by plugins. + + """ + + def __init__(self, vcs=None, breaking=False, feature=False): + baserelease.Basereleaser.__init__(self, vcs=vcs) + # Prepare some defaults for potential overriding. + if breaking: + release = 'breaking' + elif feature: + release = 'feature' + else: + release = 'normal' + self.data.update(dict( + history_header=HISTORY_HEADER, + breaking=breaking, + feature=feature, + release=release, + commit_msg=COMMIT_MSG)) + + def prepare(self): + """Prepare self.data by asking about new dev version""" + print('Checking version bump for {} release.'.format( + self.data['release'])) + if not utils.sanity_check(self.vcs): + logger.critical("Sanity check failed.") + sys.exit(1) + self._grab_version(initial=True) + self._grab_history() + # Grab and set new version. + self._grab_version() + + def execute(self): + """Make the changes and offer a commit""" + self._change_header() + self._write_version() + self._write_history() + self._diff_and_commit() + + def _grab_version(self, initial=False): + """Grab the version. + + When initial is False, ask the user for a non-development + version. When initial is True, grab the current suggestion. + + """ + original_version = self.vcs.version + logger.debug("Extracted version: %s", original_version) + if original_version is None: + logger.critical('No version found.') + sys.exit(1) + suggestion = new_version = self.data.get('new_version') + if not new_version: + # Get a suggestion. + breaking = self.data['breaking'] + feature = self.data['feature'] + # Compare the suggestion for the last tag with the current version. + # The wanted version bump may already have been done. + last_tag_version = utils.get_last_tag(self.vcs, allow_missing=True) + if last_tag_version is None: + print("No tag found. No version bump needed.") + sys.exit(0) + else: + print("Last tag: {}".format(last_tag_version)) + print("Current version: {}".format(original_version)) + minimum_version = utils.suggest_version( + last_tag_version, feature=feature, breaking=breaking) + if minimum_version <= original_version: + print("No version bump needed.") + sys.exit(0) + # A bump is needed. Get suggestion for next version. + suggestion = utils.suggest_version( + original_version, feature=feature, breaking=breaking) + if not initial: + new_version = utils.ask_version( + "Enter version", default=suggestion) + if not new_version: + new_version = suggestion + self.data['original_version'] = original_version + self.data['new_version'] = new_version + + +def datacheck(data): + """Entrypoint: ensure that the data dict is fully documented""" + utils.is_data_documented(data, documentation=DATA) + + +def main(): + parser = utils.base_option_parser() + parser.add_argument( + "--feature", + action="store_true", + dest="feature", + default=False, + help="Bump for feature release (increase minor version)") + parser.add_argument( + "--breaking", + action="store_true", + dest="breaking", + default=False, + help="Bump for breaking release (increase major version)") + options = utils.parse_options(parser) + if options.breaking and options.feature: + print('Cannot have both breaking and feature options.') + sys.exit(1) + utils.configure_logging() + bumpversion = BumpVersion( + breaking=options.breaking, feature=options.feature) + bumpversion.run() diff --git a/zest/releaser/preparedocs.py b/zest/releaser/preparedocs.py index 57f6de42..591f1b43 100644 --- a/zest/releaser/preparedocs.py +++ b/zest/releaser/preparedocs.py @@ -3,6 +3,7 @@ import os from zest.releaser import addchangelogentry +from zest.releaser import bumpversion from zest.releaser import prerelease from zest.releaser import release from zest.releaser import postrelease @@ -34,6 +35,7 @@ def prepare_entrypoint_documentation(data): ('release', release.DATA), ('postrelease', postrelease.DATA), ('addchangelogentry', addchangelogentry.DATA), + ('bumpversion', bumpversion.DATA), ): heading = '%s data dict items' % name.capitalize() result.append(heading) diff --git a/zest/releaser/prerelease.py b/zest/releaser/prerelease.py index e27977fb..f6464d06 100644 --- a/zest/releaser/prerelease.py +++ b/zest/releaser/prerelease.py @@ -40,7 +40,7 @@ 'Text that must be present in the changelog. Can be a string or a ' 'list, for example ["New:", "Fixes:"]. For a list, only one of them ' 'needs to be present.'), - 'original_version': 'Version before prereleasing (e.g. 1.0dev)', + 'original_version': 'Version before prereleasing (e.g. 1.0.dev0)', 'commit_msg': 'Message template used when committing', 'history_header': 'Header template used for 1st history header', } diff --git a/zest/releaser/tests/addchangelogentry.txt b/zest/releaser/tests/addchangelogentry.txt index 12102949..c50e0db6 100644 --- a/zest/releaser/tests/addchangelogentry.txt +++ b/zest/releaser/tests/addchangelogentry.txt @@ -1,5 +1,5 @@ -Detailed tests of prerelease.py -=============================== +Detailed tests of addchangelogentry.py +====================================== .. :doctest: .. :setup: zest.releaser.tests.functional.setup @@ -45,7 +45,7 @@ commit:: Question: OK to commit this (Y/n)? Our reply: -The changelog and setup.py are at 0.1 and indicate a release date: +The changelog and setup.py are at 0.1 and have the message:: >>> contents = (open('CHANGES.txt').read()) >>> print(contents) diff --git a/zest/releaser/tests/bumpversion.txt b/zest/releaser/tests/bumpversion.txt new file mode 100644 index 00000000..60e4c78e --- /dev/null +++ b/zest/releaser/tests/bumpversion.txt @@ -0,0 +1,94 @@ +Detailed tests of bumpversion.py +================================ + +.. :doctest: +.. :setup: zest.releaser.tests.functional.setup +.. :teardown: zest.releaser.tests.functional.teardown + +Several items are prepared for us. + +An svn repository: + + >>> repo_url + 'file://TESTREPO' + +An svn checkout of a project: + + >>> svnsourcedir + 'TESTTEMP/tha.example-svn' + >>> import os + >>> import sys + >>> os.chdir(svnsourcedir) + +Asking input on the prompt is not unittestable unless we use the prepared +testing hack in utils.py: + + >>> from zest.releaser import utils + >>> utils.TESTMODE = True + +Initially there are no tags, and we require them. In the tests the +error is ugly, but in practice it looks fine, saying no bump is needed. + + >>> from zest.releaser import bumpversion + >>> bumpversion.main() + Traceback (most recent call last): + ... + RuntimeError: SYSTEM EXIT (code=0) + +So first run the fullrelease: + + >>> from zest.releaser import fullrelease + >>> utils.test_answer_book.set_answers(['', '', '', '2.3.4', '', '', '', '', '', '', '']) + >>> fullrelease.main() + Question... + Question: Enter version [0.1]: + Our reply: 2.3.4 + ... + >>> svnhead('CHANGES.txt') + Changelog of tha.example + ======================== + + 2.3.5 (unreleased) + ------------------ + >>> svnhead('setup.py') + from setuptools import setup, find_packages + import os.path + + version = '2.3.5.dev0' + +Try bumpversion again. The first time we again get an error because +no version bump is needed: our current version is already higher than +the latest tag, and we have no feature or breaking change. In the +tests it is again ugly, but the exit code is zero, which is good. + + >>> utils.test_answer_book.set_answers(['', '', '', '', '', '']) + >>> bumpversion.main() + Traceback (most recent call last): + ... + RuntimeError: SYSTEM EXIT (code=0) + +Now a feature bump:: + + >>> sys.argv[1:] = ['--feature'] + >>> bumpversion.main() + Checking version bump for feature release. + Last tag: 2.3.4 + Current version: 2.3.5.dev0 + Question: Enter version [2.4.0.dev0]: + Our reply: + Checking data dict + Question: OK to commit this (Y/n)? + Our reply: + +Now a breaking bump:: + + >>> sys.argv[1:] = ['--breaking'] + >>> bumpversion.main() + Checking version bump for breaking release. + Last tag: 2.3.4 + Current version: 2.4.0.dev0 + Question: Enter version [3.0.0.dev0]: + Our reply: + Checking data dict + Question: OK to commit this (Y/n)? + Our reply: diff --git a/zest/releaser/tests/utils.txt b/zest/releaser/tests/utils.txt index 29953200..53b6d898 100644 --- a/zest/releaser/tests/utils.txt +++ b/zest/releaser/tests/utils.txt @@ -93,7 +93,7 @@ Keep development marker when it is there. >>> utils.suggest_version('1.0.dev0') '1.1.dev0' -Suggest versions for a feature (minor) release: +Suggest versions for a feature (minor) or breaking (major) release: >>> utils.suggest_version('1.0', feature=True) '1.1' diff --git a/zest/releaser/utils.py b/zest/releaser/utils.py index 8b8b14b1..35578f80 100644 --- a/zest/releaser/utils.py +++ b/zest/releaser/utils.py @@ -204,7 +204,7 @@ def suggest_version(current, feature=False, breaking=False): last = current_split[target] try: last = int(last) + 1 - suggestion = '.'.join([char for char in first, str(last) if char]) + suggestion = '.'.join([char for char in (first, str(last)) if char]) except ValueError: # Fall back on simply updating the last character when it is # an integer. @@ -796,11 +796,12 @@ def retry_yes_no(command): print(explanation) -def get_last_tag(vcs): +def get_last_tag(vcs, allow_missing=False): """Get last tag number, compared to current version. - Note: when this cannot get a proper tag for some reason, it may - exit the program completely. + Note: when this cannot get a proper tag for some reason, it may exit + the program completely. When no tags are found and allow_missing is + True, we return None. """ version = vcs.version if not version: @@ -808,6 +809,9 @@ def get_last_tag(vcs): sys.exit(1) available_tags = vcs.available_tags() if not available_tags: + if allow_missing: + logger.debug("No tags found.") + return logger.critical("No tags found, so we can't do anything.") sys.exit(1)