diff --git a/releasenotes/notes/stop-scanning-branch-e5a8937c248acc99.yaml b/releasenotes/notes/stop-scanning-branch-e5a8937c248acc99.yaml new file mode 100644 index 0000000..3caccc5 --- /dev/null +++ b/releasenotes/notes/stop-scanning-branch-e5a8937c248acc99.yaml @@ -0,0 +1,7 @@ +--- +features: + - Automatically stop scanning branches at the point where they + diverge from master. This avoids having release notes from older + versions, that appear on master before the branch, from showing up + in the versions from the branch. This logic is only applied to + branches created from master. diff --git a/reno/scanner.py b/reno/scanner.py index 4f8525f..559aa52 100644 --- a/reno/scanner.py +++ b/reno/scanner.py @@ -78,6 +78,64 @@ def _get_unique_id(filename): return uniqueid +def _get_branch_base(reporoot, branch): + "Return the tag at base of the branch." + # Based on + # http://stackoverflow.com/questions/1527234/finding-a-branch-point-with-git + # git rev-list $(git rev-list --first-parent \ + # ^origin/stable/newton master | tail -n1)^^! + # + # Determine the list of commits accessible from the branch we are + # supposed to be scanning, but not on master. + cmd = [ + 'git', + 'rev-list', + '--first-parent', + branch, # on the branch + '^master', # not on master + ] + try: + LOG.debug(' '.join(cmd)) + parents = utils.check_output(cmd, cwd=reporoot).strip() + if not parents: + # There are no commits on the branch, yet, so we can use + # our current-version logic. + return _get_current_version(reporoot, branch) + except subprocess.CalledProcessError as e: + LOG.warning('failed to retrieve branch base: %s [%s]', + e, e.output.strip()) + return None + parent = parents.splitlines()[-1] + LOG.debug('parent = %r', parent) + # Now get the previous commit, which should be the one we tagged + # to create the branch. + cmd = [ + 'git', + 'rev-list', + '{}^^!'.format(parent), + ] + try: + sha = utils.check_output(cmd, cwd=reporoot).strip() + LOG.debug('sha = %r', sha) + except subprocess.CalledProcessError as e: + LOG.warning('failed to retrieve branch base: %s [%s]', + e, e.output.strip()) + return None + # Now get the tag for that commit. + cmd = [ + 'git', + 'describe', + '--abbrev=0', + sha, + ] + try: + return utils.check_output(cmd, cwd=reporoot).strip() + except subprocess.CalledProcessError as e: + LOG.warning('failed to retrieve branch base: %s [%s]', + e, e.output.strip()) + return None + + # The git log output from _get_tags_on_branch() looks like this sample # from the openstack/nova repository for git 1.9.1: # @@ -164,9 +222,26 @@ def get_notes_by_version(conf): reporoot = conf.reporoot notesdir = conf.notespath branch = conf.branch + earliest_version = conf.earliest_version + collapse_pre_releases = conf.collapse_pre_releases LOG.debug('scanning %s/%s (branch=%s)' % (reporoot, notesdir, branch)) + # If the user has not told us where to stop, try to work it out + # for ourselves. If branch is set and is not "master", then we + # want to stop at the base of the branch. + if (not earliest_version) and branch and (branch != 'master'): + LOG.debug('determining earliest_version from branch') + earliest_version = _get_branch_base(reporoot, branch) + if earliest_version and collapse_pre_releases: + if PRE_RELEASE_RE.search(earliest_version): + # The earliest version won't actually be the pre-release + # that might have been tagged when the branch was created, + # but the final version. Strip the pre-release portion of + # the version number. + earliest_version = '.'.join(earliest_version.split('.')[:-1]) + LOG.debug('using earliest_version = %r', earliest_version) + # Determine all of the tags known on the branch, in their date # order. We scan the commit history in topological order to ensure # we have the commits in the right version, so we might encounter @@ -319,7 +394,7 @@ def get_notes_by_version(conf): # Combine pre-releases into the final release, if we are told to # and the final release exists. - if conf.collapse_pre_releases: + if collapse_pre_releases: collapsing = files_and_tags files_and_tags = collections.OrderedDict() for ov in versions_by_date: @@ -365,7 +440,7 @@ def get_notes_by_version(conf): trimmed[ov] = sorted(files_and_tags[ov]) # If we have been told to stop at a version, we can do that # now. - if conf.earliest_version and ov == conf.earliest_version: + if earliest_version and ov == earliest_version: break LOG.debug('[reno] found %d versions and %d files', diff --git a/reno/tests/test_scanner.py b/reno/tests/test_scanner.py index c59fdb2..810fca4 100644 --- a/reno/tests/test_scanner.py +++ b/reno/tests/test_scanner.py @@ -815,13 +815,179 @@ class BranchTest(Base): } self.assertEqual( { - '1.0.0': [self.f1], '2.0.0': [self.f2], '2.0.0-1': [f21], }, results, ) + def test_pre_release_branch_no_collapse(self): + f4 = self._add_notes_file('slug4') + self._run_git('tag', '-s', '-m', 'pre-release', '4.0.0.0rc1') + # Add a commit on master after the tag + self._add_notes_file('slug5') + # Move back to the tag and create the branch + self._run_git('checkout', '4.0.0.0rc1') + self._run_git('checkout', '-b', 'stable/4') + # Create a commit on the branch + f41 = self._add_notes_file('slug41') + log_text = self._run_git( + 'log', '--pretty=%x00%H %d', '--name-only', '--graph', + '--all', '--decorate', + ) + self.addDetail('git log', text_content(log_text)) + rev_list = self._run_git('rev-list', '--first-parent', + '^stable/4', 'master') + self.addDetail('rev-list', text_content(rev_list)) + self.c.override( + branch='stable/4', + collapse_pre_releases=False, + ) + raw_results = scanner.get_notes_by_version(self.c) + results = { + k: [f for (f, n) in v] + for (k, v) in raw_results.items() + } + self.assertEqual( + { + '4.0.0.0rc1': [f4], + '4.0.0.0rc1-1': [f41], + }, + results, + ) + + def test_pre_release_branch_collapse(self): + f4 = self._add_notes_file('slug4') + self._run_git('tag', '-s', '-m', 'pre-release', '4.0.0.0rc1') + # Add a commit on master after the tag + self._add_notes_file('slug5') + # Move back to the tag and create the branch + self._run_git('checkout', '4.0.0.0rc1') + self._run_git('checkout', '-b', 'stable/4') + # Create a commit on the branch + f41 = self._add_notes_file('slug41') + self._run_git('tag', '-s', '-m', 'release', '4.0.0') + log_text = self._run_git( + 'log', '--pretty=%x00%H %d', '--name-only', '--graph', + '--all', '--decorate', + ) + self.addDetail('git log', text_content(log_text)) + rev_list = self._run_git('rev-list', '--first-parent', + '^stable/4', 'master') + self.addDetail('rev-list', text_content(rev_list)) + self.c.override( + branch='stable/4', + collapse_pre_releases=True, + ) + raw_results = scanner.get_notes_by_version(self.c) + results = { + k: [f for (f, n) in v] + for (k, v) in raw_results.items() + } + self.assertEqual( + { + '4.0.0': [f4, f41], + }, + results, + ) + + def test_full_release_branch(self): + f4 = self._add_notes_file('slug4') + self._run_git('tag', '-s', '-m', 'release', '4.0.0') + # Add a commit on master after the tag + self._add_notes_file('slug5') + # Move back to the tag and create the branch + self._run_git('checkout', '4.0.0') + self._run_git('checkout', '-b', 'stable/4') + # Create a commit on the branch + f41 = self._add_notes_file('slug41') + log_text = self._run_git( + 'log', '--pretty=%x00%H %d', '--name-only', '--graph', + '--all', '--decorate', + ) + self.addDetail('git log', text_content(log_text)) + rev_list = self._run_git('rev-list', '--first-parent', + '^stable/4', 'master') + self.addDetail('rev-list', text_content(rev_list)) + self.c.override( + branch='stable/4', + ) + raw_results = scanner.get_notes_by_version(self.c) + results = { + k: [f for (f, n) in v] + for (k, v) in raw_results.items() + } + self.assertEqual( + { + '4.0.0': [f4], + '4.0.0-1': [f41], + }, + results, + ) + + def test_branch_tip_of_master(self): + # We have branched from master, but not added any commits to + # master. + f4 = self._add_notes_file('slug4') + self._run_git('tag', '-s', '-m', 'release', '4.0.0') + self._run_git('checkout', '-b', 'stable/4') + # Create a commit on the branch + f41 = self._add_notes_file('slug41') + f42 = self._add_notes_file('slug42') + log_text = self._run_git( + 'log', '--pretty=%x00%H %d', '--name-only', '--graph', + '--all', '--decorate', + ) + self.addDetail('git log', text_content(log_text)) + rev_list = self._run_git('rev-list', '--first-parent', + '^stable/4', 'master') + self.addDetail('rev-list', text_content(rev_list)) + self.c.override( + branch='stable/4', + ) + raw_results = scanner.get_notes_by_version(self.c) + results = { + k: [f for (f, n) in v] + for (k, v) in raw_results.items() + } + self.assertEqual( + { + '4.0.0': [f4], + '4.0.0-2': [f41, f42], + }, + results, + ) + + def test_branch_no_more_commits(self): + # We have branched from master, but not added any commits to + # our branch or to master. + f4 = self._add_notes_file('slug4') + self._run_git('tag', '-s', '-m', 'release', '4.0.0') + self._run_git('checkout', '-b', 'stable/4') + # Create a commit on the branch + log_text = self._run_git( + 'log', '--pretty=%x00%H %d', '--name-only', '--graph', + '--all', '--decorate', + ) + self.addDetail('git log', text_content(log_text)) + rev_list = self._run_git('rev-list', '--first-parent', + '^stable/4', 'master') + self.addDetail('rev-list', text_content(rev_list)) + self.c.override( + branch='stable/4', + ) + raw_results = scanner.get_notes_by_version(self.c) + results = { + k: [f for (f, n) in v] + for (k, v) in raw_results.items() + } + self.assertEqual( + { + '4.0.0': [f4], + }, + results, + ) + class GetTagsParseTest(base.TestCase):