summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Collins <rbtcollins@hp.com>2014-07-21 14:31:16 +1200
committerRobert Collins <rbtcollins@hp.com>2014-08-26 11:17:35 +1200
commitc1c99a7c317c5dc4bdac9a9f8f10e96e5a658b8f (patch)
tree460199bdb4a5905e70766e7a2386697cd895366f
parentc7e00a36de3b4134e9bbe82463087684b1c30a02 (diff)
downloadpbr-c1c99a7c317c5dc4bdac9a9f8f10e96e5a658b8f.tar.gz
Look for and process sem-ver pseudo headers in git
At the moment careful changelog review is needed by humans to determine the next version number. Semantic versioning means that all we need to know to get a reasonable default next version (and a lower bound on the next version) is a record of bugfix/deprecation/feature work and api-breaking commits. In this patch we start scanning for such headers from the git history to remove this burden from developers/project release managers. Higher versions can of course be used either via pre-versioning or by tagging the desired version. implements: blueprint pbr-semver sem-ver: feature Change-Id: Id5e8cd723d5186d1bd8c01599eae8933e6f7ea6d
-rw-r--r--doc/source/index.rst10
-rw-r--r--pbr/packaging.py39
-rw-r--r--pbr/tests/test_packaging.py59
3 files changed, 104 insertions, 4 deletions
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 897f3e5..fa77de1 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -36,6 +36,16 @@ If a given revision is tagged, that's the version.
If it's not, then we take the last tagged version number and increment it to
get a minimum target version.
+We then walk git history back to the last release. Within each commit we look
+for a sem-ver: pseudo header, and if found parse it looking for keywords.
+Unknown symbols are not an error (so that folk can't wedge pbr or break their
+tree), but we will emit an info level warning message. Known symbols:
+``feature``, ``api-break``, ``deprecation``, ``bugfix``. A missing
+sem-ver line is equivalent to ``sem-ver: bugfix``. The ``bugfix`` symbol causes
+a patch level increment to the version. The ``feature`` and ``deprecation``
+symbols cause a minor version increment. The ``api-break`` symbol causes a
+major version increment.
+
If postversioning is in use, we use the resulting version number as the target
version.
diff --git a/pbr/packaging.py b/pbr/packaging.py
index 3f35593..e2fec70 100644
--- a/pbr/packaging.py
+++ b/pbr/packaging.py
@@ -776,6 +776,43 @@ def have_sphinx():
return _have_sphinx
+def _get_increment_kwargs(git_dir, tag):
+ """Calculate the sort of semver increment needed from git history.
+
+ Every commit from HEAD to tag is consider for sem-ver metadata lines.
+ See the pbr docs for their syntax.
+
+ :return: a dict of kwargs for passing into SemanticVersion.increment.
+ """
+ result = {}
+ if tag:
+ version_spec = tag + "..HEAD"
+ else:
+ version_spec = "HEAD"
+ changelog = _run_git_command(['log', version_spec], git_dir)
+ header_len = len(' sem-ver:')
+ commands = [line[header_len:].strip() for line in changelog.split('\n')
+ if line.startswith(' sem-ver:')]
+ symbols = set()
+ for command in commands:
+ symbols.update([symbol.strip() for symbol in command.split(',')])
+
+ def _handle_symbol(symbol, symbols, impact):
+ if symbol in symbols:
+ result[impact] = True
+ symbols.discard(symbol)
+ _handle_symbol('bugfix', symbols, 'patch')
+ _handle_symbol('feature', symbols, 'minor')
+ _handle_symbol('deprecation', symbols, 'minor')
+ _handle_symbol('api-break', symbols, 'major')
+ for symbol in symbols:
+ log.info('[pbr] Unknown sem-ver symbol %r' % symbol)
+ # We don't want patch in the kwargs since it is not a keyword argument -
+ # its the default minimum increment.
+ result.pop('patch', None)
+ return result
+
+
def _get_revno_and_last_tag(git_dir):
"""Return the commit data about the most recent tag.
@@ -813,7 +850,7 @@ def _get_version_from_git_target(git_dir, target_version):
['log', '-n1', '--pretty=format:%h'], git_dir)
tag, distance = _get_revno_and_last_tag(git_dir)
last_semver = version.SemanticVersion.from_pip_string(tag or '0')
- new_version = last_semver.increment()
+ new_version = last_semver.increment(**_get_increment_kwargs(git_dir, tag))
if target_version is not None and new_version > target_version:
raise ValueError(
"git history requires a target version of %(new)s, but target "
diff --git a/pbr/tests/test_packaging.py b/pbr/tests/test_packaging.py
index 1fe8659..71ce6d3 100644
--- a/pbr/tests/test_packaging.py
+++ b/pbr/tests/test_packaging.py
@@ -73,12 +73,15 @@ class TestRepo(fixtures.Fixture):
'example@example.com'], self._basedir)
base._run_cmd(['git', 'add', '.'], self._basedir)
- def commit(self):
+ def commit(self, message_content='test commit'):
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)
+ base._run_cmd(['git', 'commit', '-m', message_content], self._basedir)
+
+ def uncommit(self):
+ base._run_cmd(['git', 'reset', '--hard', 'HEAD^'], self._basedir)
def tag(self, version):
base._run_cmd(
@@ -237,6 +240,20 @@ class TestVersions(base.BaseTestCase):
version = packaging._get_version_from_git()
self.assertThat(version, matchers.StartsWith('1.2.4.dev1.g'))
+ def test_untagged_version_minor_bump(self):
+ self.repo.commit()
+ self.repo.tag('1.2.3')
+ self.repo.commit('sem-ver: deprecation')
+ version = packaging._get_version_from_git()
+ self.assertThat(version, matchers.StartsWith('1.3.0.dev1.g'))
+
+ def test_untagged_version_major_bump(self):
+ self.repo.commit()
+ self.repo.tag('1.2.3')
+ self.repo.commit('sem-ver: api-break')
+ version = packaging._get_version_from_git()
+ self.assertThat(version, matchers.StartsWith('2.0.0.dev1.g'))
+
def test_untagged_version_has_dev_version_preversion(self):
self.repo.commit()
self.repo.tag('1.2.3')
@@ -244,7 +261,7 @@ class TestVersions(base.BaseTestCase):
version = packaging._get_version_from_git('1.2.5')
self.assertThat(version, matchers.StartsWith('1.2.5.dev1.g'))
- def test_preversion_too_low(self):
+ def test_preversion_too_low_simple(self):
# That is, the target version is either already released or not high
# enough for the semver requirements given api breaks etc.
self.repo.commit()
@@ -256,6 +273,42 @@ class TestVersions(base.BaseTestCase):
ValueError, packaging._get_version_from_git, '1.2.3')
self.assertThat(err.args[0], matchers.StartsWith('git history'))
+ def test_preversion_too_low_semver_headers(self):
+ # That is, the target version is either already released or not high
+ # enough for the semver requirements given api breaks etc.
+ self.repo.commit()
+ self.repo.tag('1.2.3')
+ self.repo.commit('sem-ver: feature')
+ # Note that we can't target 1.2.4, the feature header means we need
+ # to be working on 1.3.0 or above.
+ err = self.assertRaises(
+ ValueError, packaging._get_version_from_git, '1.2.4')
+ self.assertThat(err.args[0], matchers.StartsWith('git history'))
+
+ def test_get_kwargs_corner_cases(self):
+ # No tags:
+ git_dir = self.repo._basedir + '/.git'
+ get_kwargs = lambda tag: packaging._get_increment_kwargs(git_dir, tag)
+
+ def _check_combinations(tag):
+ self.repo.commit()
+ self.assertEqual(dict(), get_kwargs(tag))
+ self.repo.commit('sem-ver: bugfix')
+ self.assertEqual(dict(), get_kwargs(tag))
+ self.repo.commit('sem-ver: feature')
+ self.assertEqual(dict(minor=True), get_kwargs(tag))
+ self.repo.uncommit()
+ self.repo.commit('sem-ver: deprecation')
+ self.assertEqual(dict(minor=True), get_kwargs(tag))
+ self.repo.uncommit()
+ self.repo.commit('sem-ver: api-break')
+ self.assertEqual(dict(major=True), get_kwargs(tag))
+ self.repo.commit('sem-ver: deprecation')
+ self.assertEqual(dict(major=True, minor=True), get_kwargs(tag))
+ _check_combinations('')
+ self.repo.tag('1.2.3')
+ _check_combinations('1.2.3')
+
def load_tests(loader, in_tests, pattern):
return testscenarios.load_tests_apply_scenarios(loader, in_tests, pattern)