diff --git a/doc/source/sphinxext.rst b/doc/source/sphinxext.rst index b43407c..e1ad86a 100644 --- a/doc/source/sphinxext.rst +++ b/doc/source/sphinxext.rst @@ -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 ======== diff --git a/releasenotes/notes/collapse-pre-releases-0b24e0bab46d7cf1.yaml b/releasenotes/notes/collapse-pre-releases-0b24e0bab46d7cf1.yaml new file mode 100644 index 0000000..9cd1675 --- /dev/null +++ b/releasenotes/notes/collapse-pre-releases-0b24e0bab46d7cf1.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add a flag to collapse pre-release notes into their final release, + if the final release tag is present. \ No newline at end of file diff --git a/reno/lister.py b/reno/lister.py index b60267a..c71f0af 100644 --- a/reno/lister.py +++ b/reno/lister.py @@ -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: diff --git a/reno/main.py b/reno/main.py index 7f4cfb5..867c035 100644 --- a/reno/main.py +++ b/reno/main.py @@ -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() diff --git a/reno/report.py b/reno/report.py index 8f37c09..d7d3c65 100644 --- a/reno/report.py +++ b/reno/report.py @@ -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: diff --git a/reno/scanner.py b/reno/scanner.py index 261d8dd..5867e97 100644 --- a/reno/scanner.py +++ b/reno/scanner.py @@ -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() diff --git a/reno/sphinxext.py b/reno/sphinxext.py index 07359e5..f1d62e5 100644 --- a/reno/sphinxext.py +++ b/reno/sphinxext.py @@ -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() diff --git a/reno/tests/test_scanner.py b/reno/tests/test_scanner.py index fb29e97..33ddb12 100644 --- a/reno/tests/test_scanner.py +++ b/reno/tests/test_scanner.py @@ -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):