add flag to collapse pre-releases into final releases

Add a new flag to combine pre-release notes into their final release
version after that version is tagged. The flag defaults to False to
preserve the current behavior.

Change-Id: I6b9c058289f0baa3e39048b3fa78f6af81bdd83b
Signed-off-by: Doug Hellmann <doug@doughellmann.com>
This commit is contained in:
Doug Hellmann 2016-01-27 17:20:17 -05:00
parent 207776787f
commit 53891ddb1a
8 changed files with 160 additions and 4 deletions

View File

@ -46,6 +46,12 @@ Enable the extension by adding ``'reno.sphinxext'`` to the
A comma separated list of versions to include in the notes. The
default is to include all versions found on ``branch``.
*collapse-pre-releases*
A flag indicating that notes attached to pre-release versions
should be incorporated into the notes for the final release,
after the final release is tagged.
Examples
========

View File

@ -0,0 +1,4 @@
---
features:
- Add a flag to collapse pre-release notes into their final release,
if the final release tag is present.

View File

@ -25,7 +25,11 @@ def list_cmd(args):
LOG.debug('starting list')
reporoot = args.reporoot.rstrip('/') + '/'
notesdir = utils.get_notes_dir(args)
notes = scanner.get_notes_by_version(reporoot, notesdir, args.branch)
collapse = args.collapse_pre_releases
notes = scanner.get_notes_by_version(
reporoot, notesdir, args.branch,
collapse_pre_releases=collapse,
)
if args.version:
versions = args.version
else:

View File

@ -76,6 +76,12 @@ def main(argv=sys.argv[1:]):
default=None,
help='the branch to scan, defaults to the current',
)
do_list.add_argument(
'--collapse-pre-releases',
action='store_true',
default=False,
help='combine pre-releases with their final release',
)
do_list.set_defaults(func=lister.list_cmd)
do_report = subparsers.add_parser(
@ -102,6 +108,12 @@ def main(argv=sys.argv[1:]):
action='append',
help='the version(s) to include, defaults to all',
)
do_report.add_argument(
'--collapse-pre-releases',
action='store_true',
default=False,
help='combine pre-releases with their final release',
)
do_report.set_defaults(func=report.report_cmd)
args = parser.parse_args()

View File

@ -21,7 +21,11 @@ def report_cmd(args):
"Generates a release notes report"
reporoot = args.reporoot.rstrip('/') + '/'
notesdir = utils.get_notes_dir(args)
notes = scanner.get_notes_by_version(reporoot, notesdir, args.branch)
collapse = args.collapse_pre_releases
notes = scanner.get_notes_by_version(
reporoot, notesdir, args.branch,
collapse_pre_releases=collapse,
)
if args.version:
versions = args.version
else:

View File

@ -115,6 +115,9 @@ TAG_RE = re.compile('''
((?:[\d.ab]|rc)+) # digits, a, b, and rc cover regular and pre-releases
[,)] # possible trailing comma or closing paren
''', flags=re.VERBOSE | re.UNICODE)
PRE_RELEASE_RE = re.compile('''
\.(\d+(?:[ab]|rc)+\d*)$
''', flags=re.VERBOSE | re.UNICODE)
def _get_version_tags_on_branch(reporoot, branch):
@ -146,13 +149,24 @@ def _get_version_tags_on_branch(reporoot, branch):
return tags
def get_notes_by_version(reporoot, notesdir, branch=None):
def get_notes_by_version(reporoot, notesdir, branch=None,
collapse_pre_releases=False):
"""Return an OrderedDict mapping versions to lists of notes files.
The versions are presented in reverse chronological order.
Notes files are associated with the earliest version for which
they were available, regardless of whether they changed later.
:param reporoot: Path to the root of the git repository.
:type reporoot: str
:param notesdir: The directory under *reporoot* with the release notes.
:type notesdir: str
:param branch: The name of the branch to scan. Defaults to current.
:type branch: str
:param collapse_pre_releases: When true, merge pre-release versions
into the final release, if it is present.
:type collapse_pre_releases: bool
"""
LOG.debug('scanning %s/%s (branch=%s)' % (reporoot, notesdir, branch))
@ -277,6 +291,36 @@ def get_notes_by_version(reporoot, notesdir, branch=None):
LOG.debug(msg)
print(msg, file=sys.stderr)
# Combine pre-releases into the final release, if we are told to
# and the final release exists.
if collapse_pre_releases:
collapsing = files_and_tags
files_and_tags = collections.OrderedDict()
for ov in versions_by_date:
if ov not in collapsing:
# We don't need to collapse this one because there are
# no notes attached to it.
continue
pre_release_match = PRE_RELEASE_RE.search(ov)
LOG.debug('checking %r', ov)
if pre_release_match:
# Remove the trailing pre-release part of the version
# from the string.
pre_rel_str = pre_release_match.groups()[0]
canonical_ver = ov[:-len(pre_rel_str)].rstrip('.')
if canonical_ver not in versions_by_date:
# This canonical version was never tagged, so we
# do not want to collapse the pre-releases. Reset
# to the original version.
canonical_ver = ov
else:
LOG.debug('combining into %r', canonical_ver)
else:
canonical_ver = ov
if canonical_ver not in files_and_tags:
files_and_tags[canonical_ver] = []
files_and_tags[canonical_ver].extend(collapsing[ov])
# Only return the parts of files_and_tags that actually have
# filenames associated with the versions.
trimmed = collections.OrderedDict()

View File

@ -33,6 +33,7 @@ class ReleaseNotesDirective(rst.Directive):
'relnotessubdir': directives.unchanged,
'notesdir': directives.unchanged,
'version': directives.unchanged,
'collapse-pre-releases': directives.flag,
}
def run(self):
@ -50,12 +51,16 @@ class ReleaseNotesDirective(rst.Directive):
defaults.RELEASE_NOTES_SUBDIR)
notessubdir = self.options.get('notesdir', defaults.NOTES_SUBDIR)
version_opt = self.options.get('version')
collapse = self.options.get('collapse-pre-releases')
notesdir = os.path.join(relnotessubdir, notessubdir)
info('scanning %s for %s release notes' %
(os.path.join(reporoot, notesdir), branch or 'current branch'))
notes = scanner.get_notes_by_version(reporoot, notesdir, branch)
notes = scanner.get_notes_by_version(
reporoot, notesdir, branch,
collapse_pre_releases=collapse,
)
if version_opt is not None:
versions = [
v.strip()

View File

@ -519,6 +519,83 @@ class PreReleaseTest(Base):
results,
)
def test_collapse(self):
files = []
self._make_python_package()
files.append(self._add_notes_file('slug1'))
self._run_git('tag', '-s', '-m', 'alpha tag', '1.0.0.0a1')
files.append(self._add_notes_file('slug2'))
self._run_git('tag', '-s', '-m', 'beta tag', '1.0.0.0b1')
files.append(self._add_notes_file('slug3'))
self._run_git('tag', '-s', '-m', 'release candidate tag', '1.0.0.0rc1')
files.append(self._add_notes_file('slug4'))
self._run_git('tag', '-s', '-m', 'full release tag', '1.0.0')
raw_results = scanner.get_notes_by_version(
self.reporoot,
'releasenotes/notes',
collapse_pre_releases=True,
)
results = {
k: [f for (f, n) in v]
for (k, v) in raw_results.items()
}
self.assertEqual(
{'1.0.0': files,
},
results,
)
def test_collapse_without_full_release(self):
self._make_python_package()
f1 = self._add_notes_file('slug1')
self._run_git('tag', '-s', '-m', 'alpha tag', '1.0.0.0a1')
f2 = self._add_notes_file('slug2')
self._run_git('tag', '-s', '-m', 'beta tag', '1.0.0.0b1')
f3 = self._add_notes_file('slug3')
self._run_git('tag', '-s', '-m', 'release candidate tag', '1.0.0.0rc1')
raw_results = scanner.get_notes_by_version(
self.reporoot,
'releasenotes/notes',
collapse_pre_releases=True,
)
results = {
k: [f for (f, n) in v]
for (k, v) in raw_results.items()
}
self.assertEqual(
{'1.0.0.0a1': [f1],
'1.0.0.0b1': [f2],
'1.0.0.0rc1': [f3],
},
results,
)
def test_collapse_without_notes(self):
self._make_python_package()
self._run_git('tag', '-s', '-m', 'earlier tag', '0.1.0')
f1 = self._add_notes_file('slug1')
self._run_git('tag', '-s', '-m', 'alpha tag', '1.0.0.0a1')
f2 = self._add_notes_file('slug2')
self._run_git('tag', '-s', '-m', 'beta tag', '1.0.0.0b1')
f3 = self._add_notes_file('slug3')
self._run_git('tag', '-s', '-m', 'release candidate tag', '1.0.0.0rc1')
raw_results = scanner.get_notes_by_version(
self.reporoot,
'releasenotes/notes',
collapse_pre_releases=True,
)
results = {
k: [f for (f, n) in v]
for (k, v) in raw_results.items()
}
self.assertEqual(
{'1.0.0.0a1': [f1],
'1.0.0.0b1': [f2],
'1.0.0.0rc1': [f3],
},
results,
)
class MergeCommitTest(Base):