ignore null-merges

OpenStack used to use null-merges to bring final release tags from
stable branches back into the master branch. This confuses the regular
traversal because it makes that stable branch appear to be part of
master and/or the later stable branch. Update the scanner so that when
it hit one of those merge commits, it skips it and take the first parent
so it continues to traverse the branch being scanned.

Change-Id: I90722a3946f691e8f58a52e68ee455d6530f047a
Closes-Bug: #1695057
Signed-off-by: Doug Hellmann <doug@doughellmann.com>
This commit is contained in:
Doug Hellmann 2017-06-01 17:32:24 -04:00
parent ecca68b147
commit bd6fecc858
5 changed files with 169 additions and 0 deletions

View File

@ -282,6 +282,22 @@ The following options are configurable:
order in which the final report will be generated. A prelude section will
always be automatically inserted before the first element of this list.
`ignore_null_merges`
OpenStack used to use null-merges to bring final release tags from
stable branches back into the master branch. This confuses the
regular traversal because it makes that stable branch appear to be
part of master and/or the later stable branch. This option allows us
to ignore those.
When this option is set to True, any merge commits with no changes
and in which the second or later parent is tagged are considered
"null-merges" that bring the tag information into the current branch
but nothing else.
Defaults to ``True``.
Debugging
=========

View File

@ -0,0 +1,18 @@
---
features:
- |
By default, reno now ignores "null" merge commits that bring in
tags from other threads. The new configuration option
``ignore_null_merges`` controls this behavior. Setting the flag to
False restores the previous behavior in which the null-merge
commits were traversed like any other merge commit.
upgrade:
- |
The new configuration option ``ignore_null_merges`` causes the
scanner to ignore merge commits with no changes when one of the
parents being merged in has a release tag on it.
fixes:
- |
This release fixes a problem with the scanner that may have caused
it to stop scanning a branch prematurely when the tag from another
branch had been merged into the history.

View File

@ -155,6 +155,18 @@ class Config(object):
['fixes', 'Bug Fixes'],
['other', 'Other Notes'],
],
# When this option is set to True, any merge commits with no
# changes and in which the second or later parent is tagged
# are considered "null-merges" that bring the tag information
# into the current branch but nothing else.
#
# OpenStack used to use null-merges to bring final release
# tags from stable branches back into the master branch. This
# confuses the regular traversal because it makes that stable
# branch appear to be part of master and/or the later stable
# branch. This option allows us to ignore those.
'ignore_null_merges': True,
}
@classmethod

View File

@ -685,9 +685,55 @@ class Scanner(object):
todo = collections.deque()
todo.appendleft(head)
ignore_null_merges = self.conf.ignore_null_merges
if ignore_null_merges:
LOG.debug('ignoring null-merge commits')
while todo:
sha = todo.popleft()
entry = all[sha]
null_merge = False
# OpenStack used to use null-merges to bring final release
# tags from stable branches back into the master
# branch. This confuses the regular traversal because it
# makes that stable branch appear to be part of master
# and/or the later stable branch. When we hit one of those
# tags, skip it and take the first parent.
if ignore_null_merges and len(entry.commit.parents) > 1:
# Look for tags on the 2nd and later parents. The
# first parent is part of the branch we were
# originally trying to traverse, and any tags on it
# need to be kept.
for p in entry.commit.parents[1:]:
t = self._get_valid_tags_on_commit(p)
# If we have a tag being merged in, we need to
# include a check to verify that this is actually
# a null-merge (there are no changes).
if t and not entry.changes():
LOG.debug(
'treating %s as a null-merge because '
'parent %s has tag(s) %s',
sha, p, t,
)
null_merge = True
break
if null_merge:
# Make it look like the parent entries that we're
# going to skip have been emitted so the
# bookkeeping for children works properly and we
# can continue past the merge.
emitted.update(set(entry.commit.parents[1:]))
# Make it look like the current entry was emitted
# so the bookkeeping for children works properly
# and we can continue past the merge.
emitted.add(sha)
# Now set up the first parent so it is processed
# later.
first_parent = entry.commit.parents[0]
if first_parent not in todo:
todo.appendleft(first_parent)
continue
# If a node has multiple children, it is the start point
# for a branch that was merged back into the rest of the

View File

@ -1010,6 +1010,83 @@ class MergeCommitTest(Base):
)
class NullMergeTest(Base):
def setUp(self):
super(NullMergeTest, self).setUp()
self.repo.add_file('ignore-0.txt')
self.n1 = self._add_notes_file()
self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0')
# Create a branch, add a note, and tag it.
self.repo.git('checkout', '-b', 'test_ignore_null_merge')
self.n2 = self._add_notes_file()
self.repo.git('tag', '-s', '-m', 'second tag', '2.0.0')
# Move back to master and advance it.
self.repo.git('checkout', 'master')
self.repo.add_file('ignore-1.txt')
self.n3 = self._add_notes_file()
# Merge only the tag from the first branch back into master.
self.repo.git(
'merge', '--no-ff', '--strategy', 'ours', '2.0.0',
)
# Add another note file.
self.n4 = self._add_notes_file()
self.repo.git('tag', '-s', '-m', 'third tag', '3.0.0')
self.repo.git('log', '--decorate', '--oneline', '--graph', '--all')
# The results should look like:
#
# * afea344 (HEAD -> master, tag: 3.0.0) add slug-0000000000000004.yaml
# * 7bb295c Merge tag '2.0.0'
# |\
# | * 260c80b (tag: 2.0.0, test_ignore_null_merge) add slug-0000000000000002.yaml # noqa
# * | 5981ae3 add slug-0000000000000003.yaml
# * | 00f9376 add ignore-1.txt
# |/
# * d24faf9 (tag: 1.0.0) add slug-0000000000000001.yaml
# * 6c221cd add ignore-0.txt
def test_ignore(self):
# The scanner should skip over the null-merge and include the
# notes that come before the version being merged in, up to
# the base of the previous branch.
self.scanner = scanner.Scanner(self.c)
raw_results = self.scanner.get_notes_by_version()
results = {
k: [f for (f, n) in v]
for (k, v) in raw_results.items()
}
self.assertEqual(
{'1.0.0': [self.n1],
'3.0.0': [self.n3, self.n4]},
results,
)
def test_follow(self):
# The scanner should not skip over the null-merge. The output
# should include the 2.0.0 tag that was merged in, as well as
# the earlier 1.0.0 version.
self.c.override(
ignore_null_merges=False,
)
self.scanner = scanner.Scanner(self.c)
raw_results = self.scanner.get_notes_by_version()
results = {
k: [f for (f, n) in v]
for (k, v) in raw_results.items()
}
self.assertEqual(
{'1.0.0': [self.n1],
'2.0.0': [self.n2, self.n3],
'3.0.0': [self.n4]},
results,
)
class UniqueIdTest(Base):
def test_legacy(self):