From 81c200088198fb9c73e32bae23e021b7c27016aa Mon Sep 17 00:00:00 2001 From: Robert Collins Date: Fri, 14 Mar 2014 15:05:18 +1300 Subject: [PATCH] Teach pbr about post versioned dev versions. Untagged versions should not be presented as final versions, which is what was happening. The rules for dev versions are that they lead up to the next release, so to emit a dev version we have to increment the current version, then we can emit a dev version number. implements: blueprint pbr-semver sem-ver: feature Change-Id: Icf2f1999613e0d26424798697de34811b9cfc4ab --- doc/source/index.rst | 16 ++++++--- pbr/packaging.py | 61 +++++++++++++++++++++++-------- pbr/tests/base.py | 3 +- pbr/tests/test_packaging.py | 72 +++++++++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 21 deletions(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index 0385fd1..6cd5e8b 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -26,11 +26,17 @@ PBR can and does do a bunch of things for you: Version ------- -Version strings will be inferred from git. If a given revision is tagged, -that's the version. If it's not, and you don't provide a version, the version -will be very similar to git describe. If you do, then we'll assume that's the -version you are working towards, and will generate alpha version strings -based on commits since last tag and the current git sha. +Versions can be managed two ways - postversioning and preversioning. +Postversioning is the default, and preversioning is enabeld by setting +``version`` in the setup.cfg ``metadata`` section. In both cases +version strings are inferred from git. + +If a given revision is tagged, that's the version. If it's not, then either +the current version is incremented to get a target version (postversioning) or +the version set in setup.cfg metadata (preversioning) is used as the target +version. We then generate dev version strings based on the commits since the +last release and include the current git sha to disambiguate multiple dev +versions with the same number of commits since the release. .. note:: diff --git a/pbr/packaging.py b/pbr/packaging.py index 29817e1..a57b453 100644 --- a/pbr/packaging.py +++ b/pbr/packaging.py @@ -44,6 +44,7 @@ from setuptools.command import install_scripts from setuptools.command import sdist from pbr import extra_files +from pbr import version TRUE_VALUES = ('true', '1', 'yes') REQUIREMENTS_FILES = ('requirements.txt', 'tools/pip-requires') @@ -784,7 +785,7 @@ def _get_revno(git_dir): """ describe = _run_git_command(['describe', '--always'], git_dir) if "-" in describe: - return describe.rsplit("-", 2)[-2] + return int(describe.rsplit("-", 2)[-2]) # no tags found revlist = _run_git_command( @@ -792,12 +793,31 @@ def _get_revno(git_dir): return len(revlist.splitlines()) -def _get_version_from_git(pre_version): - """Return a version which is equal to the tag that's on the current - revision if there is one, or tag plus number of additional revisions - if the current revision has no tag. - """ +def _get_version_from_git_target(semver, git_dir): + """Calculate a version from a target version in git_dir. + :param semver: The version we will release next. + :param git_dir: The git directory we're working from. + :return: A version string like 1.2.3.dev1.g123124 + """ + # Drop any RC etc versions. + sha = _run_git_command( + ['log', '-n1', '--pretty=format:%h'], git_dir) + return semver.to_dev(_get_revno(git_dir), sha) + + +def _get_version_from_git(pre_version=None): + """Calculate a version string from git. + + If the revision is tagged, return that. Otherwise calculate a semantic + version description of the tree. + + The number of revisions since the last tag is included in the dev counter + in the version for untagged versions. + + :param pre_version: If supplied use this as the target version rather than + inferring one from the last tag + commit messages. + """ git_dir = _get_git_directory() if git_dir and _git_is_installed(): if pre_version: @@ -806,16 +826,24 @@ def _get_version_from_git(pre_version): ['describe', '--exact-match'], git_dir, throw_on_error=True).replace('-', '.') except Exception: - sha = _run_git_command( - ['log', '-n1', '--pretty=format:%h'], git_dir) - return "%s.dev%s.g%s" % (pre_version, _get_revno(git_dir), sha) + # not released yet - use pre_version as the target + semver = version.SemanticVersion.from_pip_string(pre_version) + return _get_version_from_git_target( + semver, git_dir).release_string() else: - description = _run_git_command( - ['describe', '--always'], git_dir).replace('-', '.') - if '.' not in description: - # Untagged tree. - description = '0.g%s' % description - return description + try: + return _run_git_command( + ['describe', '--exact-match'], git_dir, + throw_on_error=True).replace('-', '.') + except Exception: + last_version = _run_git_command( + ['describe', '--abbrev=0'], git_dir) + if not last_version: + # Untagged tree. + last_version = '0' + semver = version.SemanticVersion.from_pip_string(last_version) + return _get_version_from_git_target( + semver.increment(), git_dir).release_string() # If we don't know the version, return an empty string so at least # the downstream users of the value always have the same type of # object to work with. @@ -852,6 +880,9 @@ def get_version(package_name, pre_version=None): that a source tarball be made from our git repo - or that if someone wants to make a source tarball from a fork of our repo with additional tags in it that they understand and desire the results of doing that. + + :param pre_version: The version field from setup.cfg - if set then this + version will be the next release. """ version = os.environ.get( "PBR_VERSION", diff --git a/pbr/tests/base.py b/pbr/tests/base.py index 0586074..8a846a7 100644 --- a/pbr/tests/base.py +++ b/pbr/tests/base.py @@ -147,7 +147,8 @@ def _run_cmd(args, cwd): :return: ((stdout, stderr), returncode) """ p = subprocess.Popen( - args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd) + args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, cwd=cwd) streams = tuple(s.decode('latin1').strip() for s in p.communicate()) for content in streams: print(content) diff --git a/pbr/tests/test_packaging.py b/pbr/tests/test_packaging.py index 16194d2..d59058c 100644 --- a/pbr/tests/test_packaging.py +++ b/pbr/tests/test_packaging.py @@ -44,6 +44,7 @@ import tempfile import fixtures import mock import testscenarios +from testtools import matchers from pbr import packaging from pbr.tests import base @@ -67,11 +68,53 @@ class TestRepo(fixtures.Fixture): base._run_cmd( ['git', 'config', '--global', 'user.email', 'example@example.com'], self._basedir) + base._run_cmd( + ['git', 'config', '--global', 'user.signingkey', + 'example@example.com'], self._basedir) base._run_cmd(['git', 'add', '.'], self._basedir) def commit(self): + files = len(os.listdir(self._basedir)) + path = self._basedir + '/%d' % files + open(path, 'wt').close() + base._run_cmd(['git', 'add', path], self._basedir) base._run_cmd(['git', 'commit', '-m', 'test commit'], self._basedir) + def tag(self, version): + base._run_cmd( + ['git', 'tag', '-sm', 'test tag', version], self._basedir) + + +class GPGKeyFixture(fixtures.Fixture): + """Creates a GPG key for testing. + + It's recommended that this be used in concert with a unique home + directory. + """ + + def setUp(self): + super(GPGKeyFixture, self).setUp() + tempdir = self.useFixture(fixtures.TempDir()) + config_file = tempdir.path + '/key-config' + f = open(config_file, 'wt') + try: + f.write(""" + #%no-protection -- these would be ideal but they are documented + #%transient-key -- but not implemented in gnupg! + %no-ask-passphrase + Key-Type: RSA + Name-Real: Example Key + Name-Comment: N/A + Name-Email: example@example.com + Expire-Date: 2d + Preferences: (setpref) + %commit + """) + finally: + f.close() + base._run_cmd( + ['gpg', '--gen-key', '--batch', config_file], tempdir.path) + class TestPackagingInGitRepoWithCommit(base.BaseTestCase): @@ -173,5 +216,34 @@ class TestNestedRequirements(base.BaseTestCase): self.assertEqual(result, ['pbr']) +class TestVersions(base.BaseTestCase): + + def setUp(self): + super(TestVersions, self).setUp() + self.repo = self.useFixture(TestRepo(self.package_dir)) + self.useFixture(GPGKeyFixture()) + self.useFixture(base.DiveDir(self.package_dir)) + + def test_tagged_version_has_tag_version(self): + self.repo.commit() + self.repo.tag('1.2.3') + version = packaging._get_version_from_git('1.2.3') + self.assertEqual('1.2.3', version) + + def test_untagged_version_has_dev_version_postversion(self): + self.repo.commit() + self.repo.tag('1.2.3') + self.repo.commit() + version = packaging._get_version_from_git() + self.assertThat(version, matchers.StartsWith('1.2.4.dev1.g')) + + def test_untagged_version_has_dev_version_preversion(self): + self.repo.commit() + self.repo.tag('1.2.3') + self.repo.commit() + version = packaging._get_version_from_git('1.2.5') + self.assertThat(version, matchers.StartsWith('1.2.5.dev1.g')) + + def load_tests(loader, in_tests, pattern): return testscenarios.load_tests_apply_scenarios(loader, in_tests, pattern)